mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(radio): normalize station tags and retain valid mixed rows
This commit is contained in:
1 parent
de2138e84c
commit
32b0304c2c
9 files changed
+57
-26
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
Radio tags are lowercased and whitespace-normalized before the final 80-character
|
||||
cap, including requested-tag coverage matching. Malformed station rows are skipped
|
||||
individually; valid rows retain their accepted catalog generation. The UI snapshot
|
||||
reports a bounded `rejectedStationCount` and degraded coverage when rows were
|
||||
skipped. Malformed envelope/freshness/generation metadata and all-invalid responses
|
||||
still preserve the previous usable catalog.
|
||||
|
||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { readResponseTextCapped } from '../common/http.js';
|
||||
import {
|
||||
normalizeRadioBrowserStation,
|
||||
publicRadioStation,
|
||||
cleanRadioText,
|
||||
normalizeRadioTag,
|
||||
} from './stations.js';
|
||||
import {
|
||||
radioMirrorOrigin,
|
||||
@@ -177,8 +177,8 @@ export function createRadioProxyMiddleware({
|
||||
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(
|
||||
const stations = rows
|
||||
.filter(
|
||||
(row) =>
|
||||
row &&
|
||||
typeof row === 'object' &&
|
||||
@@ -188,18 +188,9 @@ export function createRadioProxyMiddleware({
|
||||
(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 requestedTag = normalizeRadioTag(tag);
|
||||
const requestedTagCovered =
|
||||
!requestedTag ||
|
||||
stations.some((station) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
cleanRadioText,
|
||||
normalizeRadioTag,
|
||||
isNonGlobalIpv4,
|
||||
publicRadioHttpsUrl,
|
||||
normalizeRadioBrowserStation,
|
||||
|
||||
@@ -712,6 +712,24 @@ test('unusable directory responses preserve warm client state atomically', async
|
||||
assert.equal(getRadioUIState().stale, true);
|
||||
assert.equal(getRadioUIState().degraded, true);
|
||||
}
|
||||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||||
stations: [replacement, { id: 'malformed' }], stale: false, degraded: false,
|
||||
acceptedGeneration: 2, catalogInstance: 'qa-instance-a', updatedAt: now,
|
||||
}));
|
||||
await radioLayer.update();
|
||||
assert.equal(getRadioUIState().stationCount, 1);
|
||||
assert.equal(getRadioUIState().rejectedStationCount, 1);
|
||||
assert.equal(getRadioUIState().degraded, true);
|
||||
assert.equal(getRadioUIState().stale, false);
|
||||
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 2);
|
||||
assert.equal(getRadioAcceptedCatalogSnapshot().stations[0].id, replacement.id);
|
||||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||||
stations: [{ id: 'malformed' }], stale: false, degraded: false,
|
||||
acceptedGeneration: 3, catalogInstance: 'qa-instance-a', updatedAt: now,
|
||||
}));
|
||||
await radioLayer.update();
|
||||
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 2);
|
||||
assert.equal(getRadioUIState().stale, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
radioLayer.destroy();
|
||||
|
||||
@@ -533,3 +533,11 @@ test('a mirror answering an invalid reason phrase rejects instead of crashing',
|
||||
const crashed = mockMirrorStatus(t, 200, 'OK\x7f');
|
||||
await assert.rejects(Promise.race([fetchMirror(), crashed]), TypeError);
|
||||
});
|
||||
|
||||
|
||||
test('case expansion is capped after tag normalization in the portable and server station path', () => {
|
||||
const row = normalizeRadioBrowserStation(station({tags: 'İ'.repeat(80) + ', NEWS__RADIO ,news-radio'}));
|
||||
assert.equal(row.tags[0].length, 80);
|
||||
assert.equal(row.tags[0], 'i\u0307'.repeat(40));
|
||||
assert.deepEqual(row.tags.slice(1), ['news radio']);
|
||||
});
|
||||
@@ -70,11 +70,6 @@ export function createIngestion({
|
||||
);
|
||||
if (!acceptedRows.length)
|
||||
throw new Error('Radio directory returned no usable stations');
|
||||
if (acceptedRows.length !== rows.length) {
|
||||
throw new Error(
|
||||
'Radio directory response contained malformed stations',
|
||||
);
|
||||
}
|
||||
const acceptedGeneration = body.acceptedGeneration;
|
||||
if (
|
||||
acceptedGeneration !== null &&
|
||||
@@ -147,7 +142,12 @@ export function createIngestion({
|
||||
}
|
||||
if (!repeatingAcceptedGeneration) layerState._updatedAt = updatedAt;
|
||||
}
|
||||
layerState._degraded = body.degraded;
|
||||
layerState._rejectedStationCount = Math.min(
|
||||
1000000,
|
||||
rows.length - acceptedRows.length,
|
||||
);
|
||||
layerState._degraded =
|
||||
body.degraded || layerState._rejectedStationCount > 0;
|
||||
layerState._stale = body.stale;
|
||||
layerState._error = preservingWarmCatalog
|
||||
? ERROR_REFRESH_DEGRADED
|
||||
|
||||
@@ -20,6 +20,7 @@ export function createPresentation({
|
||||
loading: layerState._loading,
|
||||
stale: layerState._stale,
|
||||
degraded: layerState._degraded,
|
||||
rejectedStationCount: layerState._rejectedStationCount,
|
||||
error: layerState._error,
|
||||
updatedAt: layerState._updatedAt,
|
||||
filter: layerState._filter,
|
||||
|
||||
@@ -25,6 +25,7 @@ export function createState({ services }) {
|
||||
layerState._stale = false;
|
||||
|
||||
layerState._degraded = false;
|
||||
layerState._rejectedStationCount = 0;
|
||||
|
||||
layerState._error = null;
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ export function cleanRadioText(value, maxLength) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Normalize a station/filter tag and cap its final, case-folded representation. */
|
||||
export function normalizeRadioTag(value) {
|
||||
return cleanRadioText(
|
||||
String(value ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/[_-]+/g, ' '),
|
||||
80,
|
||||
);
|
||||
}
|
||||
|
||||
export function isNonGlobalIpv4(hostname) {
|
||||
const pieces = hostname.split('.');
|
||||
if (pieces.length !== 4 || pieces.some((piece) => !/^\d{1,3}$/.test(piece)))
|
||||
@@ -99,13 +109,7 @@ export function normalizeRadioBrowserStation(
|
||||
if (!name) return null;
|
||||
const tags = String(raw?.tags ?? '')
|
||||
.split(',')
|
||||
.map((tag) =>
|
||||
cleanRadioText(tag, 80)
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim(),
|
||||
)
|
||||
.map(normalizeRadioTag)
|
||||
.filter(Boolean)
|
||||
.filter((tag, index, all) => all.indexOf(tag) === index)
|
||||
.slice(0, 24);
|
||||
|
||||
Reference in new issue
Block a user