mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Knip, configured with the application, package exports, scripts, QA harnesses, workers, tools and tests as entry points, reports exports that no module imports. Each was checked by hand against string-based and namespace imports. - Symbols that are still used inside their own module lose only the `export` keyword (176 constants and helpers). - Unused re-exports are removed from barrel modules (flight, military, vessel, CCTV, directions, transit history, local ADS-B and street-level policy modules). - Declarations that nothing uses at all are deleted, among them AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText, DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band constants. - Default exports that nothing imports are removed. The ones in src/data/earthquakes.js, src/data/telegeographySubmarineCables.js, src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a layer instance at import time that nothing used; those instances are no longer created. Every export of a package.json entry stays, as does TOMTOM_CREDIT, which the traffic layer reads from the credits namespace it is given.
395 lines
12 KiB
JavaScript
395 lines
12 KiB
JavaScript
import { lookup as lookupDns } from 'node:dns/promises';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
import { readResponseTextCapped } from '../common/http.js';
|
|
import {
|
|
normalizeRadioBrowserStation,
|
|
publicRadioStation,
|
|
cleanRadioText,
|
|
} from './stations.js';
|
|
import {
|
|
radioMirrorOrigin,
|
|
radioProxyDestination,
|
|
resolveRadioProxyAddresses,
|
|
fetchPinnedRadioResponse,
|
|
} from './transport.js';
|
|
import {
|
|
RADIO_DIRECTORY_CACHE_MS,
|
|
RADIO_DIRECTORY_STALE_MS,
|
|
RADIO_MIRROR_CACHE_MS,
|
|
RADIO_FETCH_TIMEOUT_MS,
|
|
RADIO_RESPONSE_MAX_BYTES,
|
|
RADIO_DIRECTORY_LIMIT,
|
|
RADIO_CATALOG_MIN_SUCCESSFUL_QUERIES,
|
|
RADIO_CATALOG_HEALTHY_MIN_STATIONS,
|
|
RADIO_USER_AGENT,
|
|
RADIO_UUID_RE,
|
|
RADIO_FALLBACK_MIRRORS,
|
|
} from './constants.js';
|
|
async function mapRadioConcurrent(values, concurrency, mapper) {
|
|
const results = new Array(values.length);
|
|
let cursor = 0;
|
|
const workers = Array.from(
|
|
{ length: Math.min(concurrency, values.length) },
|
|
async () => {
|
|
for (;;) {
|
|
const index = cursor++;
|
|
if (index >= values.length) return;
|
|
results[index] = await mapper(values[index], index);
|
|
}
|
|
},
|
|
);
|
|
await Promise.all(workers);
|
|
return results;
|
|
}
|
|
|
|
/** Create the testable Connect middleware backing `/api/radio`. */
|
|
export function createRadioProxyMiddleware({
|
|
fetchImpl = null,
|
|
lookupImpl = lookupDns,
|
|
now = Date.now,
|
|
} = {}) {
|
|
let mirrorCache = { origins: [...RADIO_FALLBACK_MIRRORS], cachedAt: 0 };
|
|
let mirrorPromise = null;
|
|
let catalogCache = null;
|
|
let catalogGeneration = 0;
|
|
// The generation counter is process-local, so it restarts from 1 with the
|
|
// server. The instance token scopes each generation sequence: a client that
|
|
// sees a new instance must treat the catalog as a fresh sequence, never as a
|
|
// repeat ("still generation 1") or a regression ("generation went backward").
|
|
const catalogInstance = randomUUID();
|
|
let servedStationIds = new Set();
|
|
let refreshPromise = null;
|
|
|
|
async function fetchJson(url, maxBytes = RADIO_RESPONSE_MAX_BYTES) {
|
|
const destination = radioProxyDestination(url);
|
|
if (!destination)
|
|
throw new Error('Radio Browser destination is not permitted');
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), RADIO_FETCH_TIMEOUT_MS);
|
|
try {
|
|
const addresses = await resolveRadioProxyAddresses(
|
|
destination.hostname,
|
|
lookupImpl,
|
|
);
|
|
const options = {
|
|
headers: { Accept: 'application/json', 'User-Agent': RADIO_USER_AGENT },
|
|
signal: controller.signal,
|
|
redirect: 'manual',
|
|
};
|
|
const response = fetchImpl
|
|
? await fetchImpl(destination.href, options)
|
|
: await fetchPinnedRadioResponse(destination, options, addresses);
|
|
if (response.status >= 300 && response.status < 400) {
|
|
try {
|
|
await response.body?.cancel?.();
|
|
} catch {
|
|
/* no-op */
|
|
}
|
|
throw new Error('Radio Browser redirects are refused');
|
|
}
|
|
if (!response.ok)
|
|
throw new Error(`Radio Browser returned ${response.status}`);
|
|
const text = await readResponseTextCapped(response, maxBytes);
|
|
return JSON.parse(text);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function mirrors() {
|
|
if (now() - mirrorCache.cachedAt < RADIO_MIRROR_CACHE_MS)
|
|
return mirrorCache.origins;
|
|
if (!mirrorPromise) {
|
|
mirrorPromise = (async () => {
|
|
try {
|
|
const rows = await fetchJson(
|
|
'https://all.api.radio-browser.info/json/servers',
|
|
256 * 1024,
|
|
);
|
|
const discovered = [
|
|
...new Set(
|
|
(Array.isArray(rows) ? rows : [])
|
|
.map((row) => radioMirrorOrigin(row?.name))
|
|
.filter(Boolean),
|
|
),
|
|
];
|
|
if (discovered.length) {
|
|
mirrorCache = {
|
|
origins: [
|
|
...discovered,
|
|
...RADIO_FALLBACK_MIRRORS.filter(
|
|
(origin) => !discovered.includes(origin),
|
|
),
|
|
],
|
|
cachedAt: now(),
|
|
};
|
|
}
|
|
} catch {
|
|
mirrorCache = { ...mirrorCache, cachedAt: now() };
|
|
}
|
|
return mirrorCache.origins;
|
|
})().finally(() => {
|
|
mirrorPromise = null;
|
|
});
|
|
}
|
|
return mirrorPromise;
|
|
}
|
|
|
|
async function fetchPath(pathname) {
|
|
let lastError = null;
|
|
for (const origin of await mirrors()) {
|
|
try {
|
|
return await fetchJson(`${origin}${pathname}`);
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
}
|
|
throw lastError || new Error('No Radio Browser mirror is available');
|
|
}
|
|
|
|
async function refreshCatalog() {
|
|
const queries = [
|
|
null,
|
|
'news',
|
|
'talk',
|
|
'weather',
|
|
'emergency',
|
|
'scanner',
|
|
'aviation',
|
|
'marine',
|
|
'traffic',
|
|
];
|
|
const outcomes = await mapRadioConcurrent(
|
|
queries,
|
|
3,
|
|
async (tag, index) => {
|
|
const params = new URLSearchParams({
|
|
has_geo_info: 'true',
|
|
is_https: 'true',
|
|
hidebroken: 'true',
|
|
order: 'clickcount',
|
|
reverse: 'true',
|
|
limit: index === 0 ? '1800' : '220',
|
|
});
|
|
if (tag) params.set('tag', tag);
|
|
try {
|
|
const rows = await fetchPath(`/json/stations/search?${params}`);
|
|
if (!Array.isArray(rows))
|
|
throw new Error('Radio Browser catalog payload was not an array');
|
|
if (
|
|
!rows.every(
|
|
(row) =>
|
|
row &&
|
|
typeof row === 'object' &&
|
|
!Array.isArray(row) &&
|
|
typeof row.stationuuid === 'string' &&
|
|
typeof row.name === 'string' &&
|
|
(typeof row.url_resolved === 'string' ||
|
|
typeof row.url === 'string'),
|
|
)
|
|
)
|
|
throw new Error(
|
|
'Radio Browser catalog contained a malformed station row',
|
|
);
|
|
const stations = rows
|
|
.map(normalizeRadioBrowserStation)
|
|
.filter(Boolean);
|
|
const requestedTag = cleanRadioText(tag, 80)
|
|
.toLocaleLowerCase()
|
|
.replace(/[_-]+/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
const requestedTagCovered =
|
|
!requestedTag ||
|
|
stations.some((station) =>
|
|
station.tags.some(
|
|
(stationTag) =>
|
|
stationTag === requestedTag ||
|
|
stationTag.includes(requestedTag),
|
|
),
|
|
);
|
|
return {
|
|
// Query coverage is based on accepted rows, not merely a payload that
|
|
// happens to match the upstream schema. Specialist responses must
|
|
// also contain an accepted station tagged for the requested category.
|
|
succeeded: stations.length > 0 && requestedTagCovered,
|
|
stations,
|
|
};
|
|
} catch {
|
|
return { succeeded: false, stations: [] };
|
|
}
|
|
},
|
|
);
|
|
const resultSets = outcomes.map((outcome) => outcome.stations);
|
|
|
|
const selected = [];
|
|
const seen = new Set();
|
|
const take = (station) => {
|
|
if (
|
|
!station ||
|
|
seen.has(station.id) ||
|
|
selected.length >= RADIO_DIRECTORY_LIMIT
|
|
)
|
|
return;
|
|
seen.add(station.id);
|
|
selected.push(station);
|
|
};
|
|
// Seed specialist station-tag queries before popularity fill so operational
|
|
// categories remain represented even when global click charts skew musical.
|
|
for (const rows of resultSets.slice(1)) rows.slice(0, 45).forEach(take);
|
|
resultSets
|
|
.flat()
|
|
.sort(
|
|
(a, b) => b.clickCount - a.clickCount || a.name.localeCompare(b.name),
|
|
)
|
|
.forEach(take);
|
|
const timestamp = now();
|
|
const successfulQueries = outcomes.filter(
|
|
(outcome) => outcome.succeeded,
|
|
).length;
|
|
const broadQueryHealthy =
|
|
outcomes[0].succeeded && outcomes[0].stations.length > 0;
|
|
const healthReasons = [];
|
|
if (!broadQueryHealthy) healthReasons.push('broad-query-unhealthy');
|
|
if (successfulQueries < RADIO_CATALOG_MIN_SUCCESSFUL_QUERIES)
|
|
healthReasons.push('query-coverage-below-policy');
|
|
if (selected.length < RADIO_CATALOG_HEALTHY_MIN_STATIONS)
|
|
healthReasons.push('station-coverage-below-policy');
|
|
const degraded = healthReasons.length > 0;
|
|
const coverage = {
|
|
successfulQueries,
|
|
totalQueries: queries.length,
|
|
stationCount: selected.length,
|
|
healthyStationMinimum: RADIO_CATALOG_HEALTHY_MIN_STATIONS,
|
|
};
|
|
const nextCatalog = {
|
|
cachedAt: timestamp,
|
|
updatedAt: new Date(timestamp).toISOString(),
|
|
stations: selected.map(publicRadioStation),
|
|
stationIds: new Set(selected.map((station) => station.id)),
|
|
degraded,
|
|
degradedReason: degraded ? healthReasons.join(',') : null,
|
|
coverage,
|
|
};
|
|
if (degraded && catalogCache) {
|
|
const error = new Error(
|
|
'Radio Browser catalog refresh did not meet health policy',
|
|
);
|
|
error.radioCatalogDegraded = true;
|
|
error.radioDegradedReason = nextCatalog.degradedReason;
|
|
error.radioCoverage = coverage;
|
|
throw error;
|
|
}
|
|
if (degraded && !selected.length) {
|
|
const error = new Error(
|
|
'Radio Browser catalog refresh returned no usable stations',
|
|
);
|
|
error.radioCatalogDegraded = true;
|
|
error.radioDegradedReason = nextCatalog.degradedReason;
|
|
error.radioCoverage = coverage;
|
|
throw error;
|
|
}
|
|
if (degraded) {
|
|
servedStationIds = nextCatalog.stationIds;
|
|
return { ...nextCatalog, acceptedGeneration: null };
|
|
}
|
|
catalogCache = {
|
|
...nextCatalog,
|
|
acceptedGeneration: ++catalogGeneration,
|
|
};
|
|
servedStationIds = catalogCache.stationIds;
|
|
return catalogCache;
|
|
}
|
|
|
|
async function getCatalog() {
|
|
if (
|
|
catalogCache &&
|
|
now() - catalogCache.cachedAt < RADIO_DIRECTORY_CACHE_MS
|
|
) {
|
|
return { ...catalogCache, stale: false };
|
|
}
|
|
if (!refreshPromise) {
|
|
refreshPromise = refreshCatalog().finally(() => {
|
|
refreshPromise = null;
|
|
});
|
|
}
|
|
try {
|
|
return { ...(await refreshPromise), stale: false };
|
|
} catch (error) {
|
|
if (
|
|
catalogCache &&
|
|
now() - catalogCache.cachedAt <= RADIO_DIRECTORY_STALE_MS
|
|
) {
|
|
return {
|
|
...catalogCache,
|
|
stale: true,
|
|
degraded: true,
|
|
degradedReason: error?.radioDegradedReason || 'refresh-failed',
|
|
coverage: error?.radioCoverage || catalogCache.coverage,
|
|
};
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function sendJson(res, status, body) {
|
|
res.writeHead(status, {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
'Cache-Control': 'no-store',
|
|
});
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
return async function radioProxyMiddleware(req, res) {
|
|
const requestUrl = new URL(req.url || '/', 'http://localhost');
|
|
if (requestUrl.pathname === '/stations') {
|
|
if (req.method !== 'GET') {
|
|
res.writeHead(405, { Allow: 'GET', 'Cache-Control': 'no-store' });
|
|
res.end();
|
|
return;
|
|
}
|
|
try {
|
|
const catalog = await getCatalog();
|
|
sendJson(res, 200, {
|
|
stations: catalog.stations,
|
|
updatedAt: catalog.updatedAt,
|
|
stale: catalog.stale,
|
|
degraded: Boolean(catalog.degraded),
|
|
degradedReason: catalog.degradedReason || null,
|
|
coverage: catalog.coverage || null,
|
|
acceptedGeneration: catalog.acceptedGeneration ?? null,
|
|
catalogInstance,
|
|
});
|
|
} catch (error) {
|
|
sendJson(res, 503, {
|
|
error: 'Radio directory is temporarily unavailable',
|
|
degraded: Boolean(error?.radioCatalogDegraded),
|
|
degradedReason: error?.radioDegradedReason || null,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const clickMatch = requestUrl.pathname.match(/^\/click\/([0-9a-f-]+)$/i);
|
|
if (clickMatch) {
|
|
if (req.method !== 'POST') {
|
|
res.writeHead(405, { Allow: 'POST', 'Cache-Control': 'no-store' });
|
|
res.end();
|
|
return;
|
|
}
|
|
const id = clickMatch[1].toLowerCase();
|
|
if (!RADIO_UUID_RE.test(id) || !servedStationIds.has(id)) {
|
|
sendJson(res, 404, { error: 'Unknown radio station' });
|
|
return;
|
|
}
|
|
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
res.end();
|
|
void fetchPath(`/json/url/${id}`).catch(() => {});
|
|
return;
|
|
}
|
|
|
|
sendJson(res, 404, { error: 'Unknown radio route' });
|
|
};
|
|
}
|