|
|
|
@ -468,7 +468,7 @@ const DXSPIDER_PROXY_URL = process.env.DXSPIDER_PROXY_URL || 'https://dxspider-p
|
|
|
|
|
|
|
|
|
|
|
|
// Cache for DX Spider telnet spots (to avoid excessive connections)
|
|
|
|
// Cache for DX Spider telnet spots (to avoid excessive connections)
|
|
|
|
let dxSpiderCache = { spots: [], timestamp: 0 };
|
|
|
|
let dxSpiderCache = { spots: [], timestamp: 0 };
|
|
|
|
const DXSPIDER_CACHE_TTL = 60000; // 60 seconds cache
|
|
|
|
const DXSPIDER_CACHE_TTL = 90000; // 90 seconds cache - reduces reconnection frequency
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
const source = (req.query.source || 'auto').toLowerCase();
|
|
|
|
const source = (req.query.source || 'auto').toLowerCase();
|
|
|
|
@ -561,25 +561,54 @@ app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper function for DX Spider (telnet-based, works locally/Pi)
|
|
|
|
// Helper function for DX Spider (telnet-based, works locally/Pi)
|
|
|
|
|
|
|
|
// Multiple nodes for failover
|
|
|
|
|
|
|
|
const DXSPIDER_NODES = [
|
|
|
|
|
|
|
|
{ host: 'dxspider.co.uk', port: 7300 },
|
|
|
|
|
|
|
|
{ host: 'w6kk.no-ip.org', port: 7300 },
|
|
|
|
|
|
|
|
{ host: 'dxc.nc7j.com', port: 7373 },
|
|
|
|
|
|
|
|
{ host: 'dx.k3lr.com', port: 7300 }
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchDXSpider() {
|
|
|
|
async function fetchDXSpider() {
|
|
|
|
// Check cache first
|
|
|
|
// Check cache first (use longer cache to reduce connection attempts)
|
|
|
|
if (Date.now() - dxSpiderCache.timestamp < DXSPIDER_CACHE_TTL && dxSpiderCache.spots.length > 0) {
|
|
|
|
if (Date.now() - dxSpiderCache.timestamp < DXSPIDER_CACHE_TTL && dxSpiderCache.spots.length > 0) {
|
|
|
|
console.log('[DX Cluster] DX Spider: returning', dxSpiderCache.spots.length, 'cached spots');
|
|
|
|
console.log('[DX Cluster] DX Spider: returning', dxSpiderCache.spots.length, 'cached spots');
|
|
|
|
return dxSpiderCache.spots;
|
|
|
|
return dxSpiderCache.spots;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Try each node until one succeeds
|
|
|
|
|
|
|
|
for (const node of DXSPIDER_NODES) {
|
|
|
|
|
|
|
|
const result = await tryDXSpiderNode(node);
|
|
|
|
|
|
|
|
if (result && result.length > 0) {
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[DX Cluster] DX Spider: all nodes failed');
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function tryDXSpiderNode(node) {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
const spots = [];
|
|
|
|
const spots = [];
|
|
|
|
let buffer = '';
|
|
|
|
let buffer = '';
|
|
|
|
let loginSent = false;
|
|
|
|
let loginSent = false;
|
|
|
|
let commandSent = false;
|
|
|
|
let commandSent = false;
|
|
|
|
|
|
|
|
let resolved = false;
|
|
|
|
|
|
|
|
|
|
|
|
const client = new net.Socket();
|
|
|
|
const client = new net.Socket();
|
|
|
|
client.setTimeout(15000);
|
|
|
|
client.setTimeout(12000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cleanup = () => {
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
|
|
|
resolved = true;
|
|
|
|
|
|
|
|
try { client.destroy(); } catch(e) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Try connecting to DX Spider node
|
|
|
|
// Try connecting to DX Spider node
|
|
|
|
client.connect(7300, 'dxspider.co.uk', () => {
|
|
|
|
client.connect(node.port, node.host, () => {
|
|
|
|
console.log('[DX Cluster] DX Spider: connected to dxspider.co.uk:7300');
|
|
|
|
console.log(`[DX Cluster] DX Spider: connected to ${node.host}:${node.port}`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
client.on('data', (data) => {
|
|
|
|
client.on('data', (data) => {
|
|
|
|
@ -589,7 +618,6 @@ app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
if (!loginSent && (buffer.includes('login:') || buffer.includes('Please enter your call') || buffer.includes('enter your callsign'))) {
|
|
|
|
if (!loginSent && (buffer.includes('login:') || buffer.includes('Please enter your call') || buffer.includes('enter your callsign'))) {
|
|
|
|
loginSent = true;
|
|
|
|
loginSent = true;
|
|
|
|
client.write('GUEST\r\n');
|
|
|
|
client.write('GUEST\r\n');
|
|
|
|
console.log('[DX Cluster] DX Spider: sent login');
|
|
|
|
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -597,14 +625,14 @@ app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
if (loginSent && !commandSent && (buffer.includes('Hello') || buffer.includes('de ') || buffer.includes('>') || buffer.includes('GUEST'))) {
|
|
|
|
if (loginSent && !commandSent && (buffer.includes('Hello') || buffer.includes('de ') || buffer.includes('>') || buffer.includes('GUEST'))) {
|
|
|
|
commandSent = true;
|
|
|
|
commandSent = true;
|
|
|
|
setTimeout(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
client.write('sh/dx 25\r\n');
|
|
|
|
client.write('sh/dx 25\r\n');
|
|
|
|
console.log('[DX Cluster] DX Spider: sent sh/dx 25');
|
|
|
|
}
|
|
|
|
}, 1000);
|
|
|
|
}, 1000);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse DX spots from the output
|
|
|
|
// Parse DX spots from the output
|
|
|
|
// Format: DX de W3LPL: 14195.0 TI5/AA8HH FT8 -09 dB 1234Z
|
|
|
|
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
for (const line of lines) {
|
|
|
|
for (const line of lines) {
|
|
|
|
if (line.includes('DX de ')) {
|
|
|
|
if (line.includes('DX de ')) {
|
|
|
|
@ -639,40 +667,51 @@ app.get('/api/dxcluster/spots', async (req, res) => {
|
|
|
|
// If we have enough spots, close connection
|
|
|
|
// If we have enough spots, close connection
|
|
|
|
if (spots.length >= 20) {
|
|
|
|
if (spots.length >= 20) {
|
|
|
|
client.write('bye\r\n');
|
|
|
|
client.write('bye\r\n');
|
|
|
|
setTimeout(() => client.destroy(), 500);
|
|
|
|
setTimeout(cleanup, 500);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
client.on('timeout', () => {
|
|
|
|
client.on('timeout', () => {
|
|
|
|
console.log('[DX Cluster] DX Spider: timeout');
|
|
|
|
cleanup();
|
|
|
|
client.destroy();
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
client.on('error', (err) => {
|
|
|
|
client.on('error', (err) => {
|
|
|
|
console.error('[DX Cluster] DX Spider error:', err.message);
|
|
|
|
// Only log unexpected errors, not connection resets (they're common)
|
|
|
|
client.destroy();
|
|
|
|
if (!err.message.includes('ECONNRESET') && !err.message.includes('ETIMEDOUT')) {
|
|
|
|
|
|
|
|
console.error(`[DX Cluster] DX Spider ${node.host} error:`, err.message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
client.on('close', () => {
|
|
|
|
client.on('close', () => {
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
|
|
|
resolved = true;
|
|
|
|
if (spots.length > 0) {
|
|
|
|
if (spots.length > 0) {
|
|
|
|
console.log('[DX Cluster] DX Spider:', spots.length, 'spots');
|
|
|
|
console.log('[DX Cluster] DX Spider:', spots.length, 'spots from', node.host);
|
|
|
|
dxSpiderCache = { spots: spots, timestamp: Date.now() };
|
|
|
|
dxSpiderCache = { spots: spots, timestamp: Date.now() };
|
|
|
|
resolve(spots);
|
|
|
|
resolve(spots);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
console.log('[DX Cluster] DX Spider: no spots received');
|
|
|
|
|
|
|
|
resolve(null);
|
|
|
|
resolve(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback timeout - close after 20 seconds regardless
|
|
|
|
// Fallback timeout - close after 15 seconds regardless
|
|
|
|
setTimeout(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
if (spots.length > 0) {
|
|
|
|
if (spots.length > 0) {
|
|
|
|
client.destroy();
|
|
|
|
resolved = true;
|
|
|
|
} else if (client.readable) {
|
|
|
|
console.log('[DX Cluster] DX Spider:', spots.length, 'spots from', node.host);
|
|
|
|
client.destroy();
|
|
|
|
dxSpiderCache = { spots: spots, timestamp: Date.now() };
|
|
|
|
|
|
|
|
resolve(spots);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
|
|
|
resolved = true;
|
|
|
|
resolve(null);
|
|
|
|
resolve(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, 20000);
|
|
|
|
}
|
|
|
|
|
|
|
|
}, 15000);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|