1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +01:00
This commit is contained in:
NoobishSVK
2024-07-15 22:21:47 +02:00
7 changed files with 223 additions and 41 deletions

View File

@@ -22,24 +22,41 @@ function parsePluginConfig(filePath) {
// Check if pluginConfig has frontEndPath defined // Check if pluginConfig has frontEndPath defined
if (pluginConfig.frontEndPath) { if (pluginConfig.frontEndPath) {
const sourcePath = path.join(path.dirname(filePath), 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 // Check if the destination directory exists, if not, create it
if (!fs.existsSync(destinationDir)) { if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
} }
// Copy the file to the destination directory
const destinationFile = path.join(destinationDir, path.basename(sourcePath)); const destinationFile = path.join(destinationDir, path.basename(sourcePath));
fs.copyFileSync(sourcePath, destinationFile);
setTimeout(function() { // Platform-specific handling for symlinks/junctions
consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); if (process.platform !== 'win32') {
}, 500) // 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 { } else {
console.error(`Error: frontEndPath is not defined in ${filePath}`); console.error(`Error: frontEndPath is not defined in ${filePath}`);
} }
} catch (err) { } catch (err) {
console.error(`Error parsing plugin config from ${filePath}: ${err}`); console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
} }
return pluginConfig; return pluginConfig;
@@ -62,9 +79,37 @@ function collectPluginConfigs() {
return pluginConfigs; 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 // Usage example
const allPluginConfigs = collectPluginConfigs(); const allPluginConfigs = collectPluginConfigs();
createLinks();
module.exports = { module.exports = {
allPluginConfigs allPluginConfigs
} };

View File

@@ -6,6 +6,9 @@ let cachedData = {};
let lastFetchTime = 0; let lastFetchTime = 0;
const fetchInterval = 3000; const fetchInterval = 3000;
const esSwitchCache = {"lastCheck":0, "esSwitch":false};
const esFetchInterval = 300000;
// Fetch data from maps // Fetch data from maps
function fetchTx(freq, piCode, rdsPs) { function fetchTx(freq, piCode, rdsPs) {
const now = Date.now(); const now = Date.now();
@@ -46,6 +49,7 @@ function processData(data, piCode, rdsPs) {
let maxScore = -Infinity; // Initialize maxScore with a very low value let maxScore = -Infinity; // Initialize maxScore with a very low value
let txAzimuth; let txAzimuth;
let maxDistance; let maxDistance;
let esMode = checkEs();
for (const cityId in data.locations) { for (const cityId in data.locations) {
const city = data.locations[cityId]; const city = data.locations[cityId];
@@ -53,7 +57,11 @@ function processData(data, piCode, rdsPs) {
for (const station of city.stations) { 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())) { 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 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) { if (score > maxScore) {
maxScore = score; maxScore = score;
txAzimuth = distance.azimuth; 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) { function haversine(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth radius in kilometers const R = 6371; // Earth radius in kilometers
const dLat = deg2rad(lat2 - lat1); const dLat = deg2rad(lat2 - lat1);

View File

@@ -433,7 +433,7 @@
FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span> FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span>
</p> </p>
<p class="text-small m-0 color-3">by <a href="https://noobish.eu" target="_blank">Noobish</a>, <a href="https://fmdx.pl" target="_blank">kkonradpl</a> & the OpenRadio community.</p> <p class="text-small m-0 color-3">by <a href="https://noobish.eu" target="_blank">Noobish</a>, <a href="https://fmdx.pl" target="_blank">kkonradpl</a> & the OpenRadio community.</p>
<span class="text-small" style="color: var(--color-3);">[<a href="https://list.fmdx.pl" target="_blank">Receiver Map</a>]</span> <span class="text-small" style="color: var(--color-3);">[<a href="https://servers.fmdx.org/" target="_blank">Receiver Map</a>]</span>
<br> <br>
<br> <br>
<% if(ownerContact){ %> <% if(ownerContact){ %>
@@ -486,6 +486,7 @@
</div> </div>
</div> </div>
<script src="js/websocket.js"></script>
<script src="js/webserver.js"></script> <script src="js/webserver.js"></script>
<% plugins?.forEach(function(plugin) { %> <% plugins?.forEach(function(plugin) { %>
<script src="js/plugins/<%= plugin %>"></script> <script src="js/plugins/<%= plugin %>"></script>

View File

@@ -45,6 +45,7 @@ var _3LAS = /** @class */ (function () {
}; };
_3LAS.prototype.Start = function () { _3LAS.prototype.Start = function () {
this.ConnectivityFlag = false; this.ConnectivityFlag = false;
this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug
// This is stupid, but required for Android.... thanks Google :( // This is stupid, but required for Android.... thanks Google :(
if (this.WakeLock) if (this.WakeLock)
@@ -128,11 +129,50 @@ var _3LAS = /** @class */ (function () {
this.ConnectivityFlag = false; this.ConnectivityFlag = false;
if (this.ConnectivityCallback) if (this.ConnectivityCallback)
this.ConnectivityCallback(false); 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) { _3LAS.prototype.OnSocketDataReady = function (data) {
this.Fallback.OnSocketDataReady(data); this.Fallback.OnSocketDataReady(data);
}; };
return _3LAS; return _3LAS;
}()); }());

View File

@@ -1,43 +1,69 @@
const DefaultVolume = 0.5; const DefaultVolume = 0.5;
let Stream; let Stream;
let shouldReconnect = true;
let newVolumeGlobal = 1;
function Init(_ev) { 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 $(".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 $("#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) { 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) { function OnPlayButtonClick(_ev) {
const $playbutton = $('.playbutton'); const $playbutton = $('.playbutton');
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); if (Stream) {
console.log("Stopping stream...");
if (Stream.ConnectivityFlag) { shouldReconnect = false;
Stream.Stop(); destroyStream();
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
} else { } else {
console.log("Starting stream...");
shouldReconnect = true;
createStream();
Stream.Start(); Stream.Start();
$playbutton.addClass('bg-gray').prop('disabled', true); $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
setTimeout(() => {
$playbutton.removeClass('bg-gray').prop('disabled', false);
}, 3000);
} }
$playbutton.addClass('bg-gray').prop('disabled', true);
setTimeout(() => {
$playbutton.removeClass('bg-gray').prop('disabled', false);
}, 3000);
} }
function updateVolume() { 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); $(document).ready(Init);

View File

@@ -1,7 +1,7 @@
var url = new URL('text', window.location.href); // WebSocket connection located in ./websocket.js
url.protocol = url.protocol.replace('http', 'ws');
var socketAddress = url.href;
var socket = new WebSocket(socketAddress);
var parsedData, signalChart, previousFreq; var parsedData, signalChart, previousFreq;
var signalData = []; var signalData = [];
var data = []; var data = [];
@@ -10,7 +10,7 @@ let updateCounter = 0;
const europe_programmes = [ const europe_programmes = [
"No PTY", "News", "Current Affairs", "Info", "No PTY", "News", "Current Affairs", "Info",
"Sport", "Education", "Drama", "Culture", "Science", "Varied", "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", "Serious Classical", "Other Music", "Weather", "Finance",
"Children's Programmes", "Social Affairs", "Religion", "Phone-in", "Children's Programmes", "Social Affairs", "Religion", "Phone-in",
"Travel", "Leisure", "Jazz Music", "Country Music", "National Music", "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 // Add a delay of 500 milliseconds before creating and appending the tooltip
$(this).data('timeout', setTimeout(() => { $(this).data('timeout', setTimeout(() => {
var tooltip = $('<div class="tooltiptext"></div>').html(tooltipText); var tooltip = $('<div class="tooltiptext"></div>').html(tooltipText);
$('body').append(tooltip); if ($('.tooltiptext').length === 0) { $('body').append(tooltip); } // Don't allow more than one tooltip
var posX = e.pageX; var posX = e.pageX;
var posY = e.pageY; var posY = e.pageY;
@@ -888,6 +888,7 @@ function initTooltips() {
// Clear the timeout if the mouse leaves before the delay completes // Clear the timeout if the mouse leaves before the delay completes
clearTimeout($(this).data('timeout')); clearTimeout($(this).data('timeout'));
$('.tooltiptext').remove(); $('.tooltiptext').remove();
setTimeout(() => { $('.tooltiptext').remove(); }, 500); // Ensure no tooltips remain stuck
}).mousemove(function(e){ }).mousemove(function(e){
var tooltipWidth = $('.tooltiptext').outerWidth(); var tooltipWidth = $('.tooltiptext').outerWidth();
var tooltipHeight = $('.tooltiptext').outerHeight(); var tooltipHeight = $('.tooltiptext').outerHeight();

30
web/js/websocket.js Normal file
View File

@@ -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;