fix(radio): canonicalize priority country aliases before querying

This commit is contained in:
Said Turksever committed 2026-11-03 14:50:23 +00:00
1 parent aff540ad39
commit 94e4c2176e
4 files changed
+36 -4

No files matched your search

+2 -1
View File
@@ -1,5 +1,6 @@
- Add optional keyless Radio priority-country coverage with up to 40 stations - Add optional keyless Radio priority-country coverage with up to 40 stations
per configured country inside the existing 750-station cap. Country query per configured country inside the existing 750-station cap; the UK alias uses
the canonical GB code. Country query
failures do not block a healthy global catalog. Bound mirror attempts to three failures do not block a healthy global catalog. Bound mirror attempts to three
and retry failed or partial refreshes after 60 seconds on all installations, and retry failed or partial refreshes after 60 seconds on all installations,
including those without priority countries configured (m3ayush, #1206). including those without priority countries configured (m3ayush, #1206).
+1
View File
@@ -6315,6 +6315,7 @@ play action; the source does not relay or record streams.
Optional `RADIO_PRIORITY_COUNTRIES` is a server-owned comma-separated list of Optional `RADIO_PRIORITY_COUNTRIES` is a server-owned comma-separated list of
up to eight ISO alpha-2 codes (for example `IN,PL`). Restart after changing it. up to eight ISO alpha-2 codes (for example `IN,PL`). Restart after changing it.
The `UK` alias is canonicalized to `GB` before deduplication and querying.
It needs no API key and adds no POWER UP entry. An empty list preserves the It needs no API key and adds no POWER UP entry. An empty list preserves the
existing global/tag-based directory. Invalid configuration fails at startup. existing global/tag-based directory. Invalid configuration fails at startup.
+5 -3
View File
@@ -39,12 +39,14 @@ export function parseRadioPriorityCountries(value = '') {
const entries = Array.isArray(value) ? value : String(value).split(','); const entries = Array.isArray(value) ? value : String(value).split(',');
const countries = []; const countries = [];
for (const entry of entries) { for (const entry of entries) {
const code = String(entry).trim().toUpperCase(); const input = String(entry).trim().toUpperCase();
if (!code) continue; if (!input) continue;
if (!/^[A-Z]{2}$/.test(code) || !normalizeRadioCountryInput(code).valid) const normalized = normalizeRadioCountryInput(input);
if (!/^[A-Z]{2}$/.test(input) || !normalized.valid)
throw new TypeError( throw new TypeError(
'RADIO_PRIORITY_COUNTRIES requires ISO alpha-2 country codes', 'RADIO_PRIORITY_COUNTRIES requires ISO alpha-2 country codes',
); );
const code = normalized.code;
if (!countries.includes(code)) countries.push(code); if (!countries.includes(code)) countries.push(code);
} }
if (countries.length > PRIORITY_COUNTRY_LIMIT) if (countries.length > PRIORITY_COUNTRY_LIMIT)
+28
View File
@@ -545,11 +545,39 @@ test('priority-country configuration is bounded, normalized and validated before
const { parseRadioPriorityCountries } = await import('../../server/providers/radio/catalog.js'); const { parseRadioPriorityCountries } = await import('../../server/providers/radio/catalog.js');
assert.deepEqual(parseRadioPriorityCountries(' in, PL,in, '), ['IN', 'PL']); assert.deepEqual(parseRadioPriorityCountries(' in, PL,in, '), ['IN', 'PL']);
assert.deepEqual(parseRadioPriorityCountries(''), []); assert.deepEqual(parseRadioPriorityCountries(''), []);
assert.deepEqual(parseRadioPriorityCountries('uk,GB,UK'), ['GB']);
assert.deepEqual(parseRadioPriorityCountries('US,CA,IN,PL,DE,FR,UK,GB,JP'), ['US', 'CA', 'IN', 'PL', 'DE', 'FR', 'GB', 'JP']);
for (const value of ['ZZ', 'India', 'IN&limit=9999', 'US,CA,IN,PL,DE,FR,GB,JP,AU']) { for (const value of ['ZZ', 'India', 'IN&limit=9999', 'US,CA,IN,PL,DE,FR,GB,JP,AU']) {
assert.throws(() => createProxy({ priorityCountries: value }), /RADIO_PRIORITY_COUNTRIES/); assert.throws(() => createProxy({ priorityCountries: value }), /RADIO_PRIORITY_COUNTRIES/);
} }
}); });
test('UK aliases query and report GB once and retain British stations', async () => {
const countryQueries = [];
const middleware = createProxy({
priorityCountries: 'UK,GB',
fetchImpl: async (url) => {
const parsed = new URL(url);
if (parsed.pathname === '/json/servers')
return responseJson([{ name: 'de1.api.radio-browser.info' }]);
const country = parsed.searchParams.get('countrycode');
if (country) {
countryQueries.push(country);
return responseJson([station({ country: 'United Kingdom', countrycode: 'GB' })]);
}
return responseJson(healthyRowsForQuery(url));
},
});
const response = await invoke(middleware, '/stations');
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.deepEqual(countryQueries, ['GB']);
assert.deepEqual(body.coverage.priorityCountries, [
{ countryCode: 'GB', succeeded: true, stationCount: 1 },
]);
assert.equal(body.stations.find((row) => row.id === UUID)?.countryCode, 'GB');
});
test('low-click country stations survive a full global catalog, with bounded cached queries and deduplication', async () => { test('low-click country stations survive a full global catalog, with bounded cached queries and deduplication', async () => {
const calls = []; const calls = [];
let active = 0; let active = 0;