refactor: expose portable source record protocols

This commit is contained in:
Sameh Khamis committed 2026-09-14 19:36:32 -07:00
1 parent 6fa25c349a
commit ea6c8653b3
12 files changed
+370 -192

No files matched your search

+5 -29
View File
@@ -1,3 +1,8 @@
import {
normalizeFeedType,
isVideoFeedType,
} from '../../../src/sources/cctvTypes.js';
export { normalizeFeedType, isVideoFeedType };
import { directionToHeading } from '../../../src/data/directionText.js';
import { haversineKm } from '../common/geo.js';
/**
@@ -31,35 +36,6 @@ export function escapeXml(text) {
.replace(/'/g, ''');
}
/**
* Canonicalize a CCTV feed type string to one of:
* 'image', 'mjpeg', 'mp4', 'webm', 'hls', or pass-through.
*
* @param {string} value - Raw feed type (e.g. 'jpeg', 'mjpg', 'video', 'stream').
* @returns {string} Normalized feed type.
*/
export function normalizeFeedType(value) {
const raw = String(value || '')
.trim()
.toLowerCase();
if (!raw) return 'image';
if (raw === 'jpeg' || raw === 'jpg' || raw === 'png') return 'image';
if (raw === 'mjpg') return 'mjpeg';
if (raw === 'video') return 'mp4';
if (raw === 'stream') return 'hls';
return raw;
}
/**
* Check whether a normalized feed type represents streaming video.
*
* @param {string} feedType
* @returns {boolean}
*/
export function isVideoFeedType(feedType) {
return feedType === 'mp4' || feedType === 'webm' || feedType === 'hls';
}
/**
* Coerce a value to a finite number, returning fallback if NaN/Infinity.
*
+1 -2
View File
@@ -10,8 +10,7 @@ export const RADIO_CATALOG_HEALTHY_MIN_STATIONS = Math.ceil(
);
export const RADIO_USER_AGENT =
'GodsEyeView/1.0 (Radio Browser directory client)';
export const RADIO_UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export { RADIO_UUID_RE } from '../../../src/sources/radioBrowser.js';
export const RADIO_FALLBACK_MIRRORS = Object.freeze([
'https://de1.api.radio-browser.info',
'https://de2.api.radio-browser.info',
+7 -155
View File
@@ -1,155 +1,7 @@
import { RADIO_UUID_RE } from './constants.js';
import { normalizeRadioCountryInput } from '../../../src/data/radioCountry.js';
export function cleanRadioText(value, maxLength) {
return String(value ?? '')
.replace(/[\u0000-\u001f\u007f]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, maxLength)
.trim();
}
export function isNonGlobalIpv4(hostname) {
const pieces = hostname.split('.');
if (pieces.length !== 4 || pieces.some((piece) => !/^\d{1,3}$/.test(piece)))
return false;
const values = pieces.map(Number);
if (values.some((value) => value > 255)) return true;
const [a, b, c] = values;
return (
a === 0 ||
a === 10 ||
a === 127 ||
a >= 224 ||
(a === 100 && b >= 64 && b <= 127) ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 0) ||
(a === 192 && b === 88 && c === 99) ||
(a === 192 && b === 168) ||
(a === 198 && (b === 18 || b === 19)) ||
(a === 198 && b === 51 && c === 100) ||
(a === 203 && b === 0 && c === 113)
);
}
/** Return a normalized public HTTPS URL, or null for local/private targets. */
export function publicRadioHttpsUrl(value) {
try {
const url = new URL(String(value ?? ''));
const hostname = url.hostname
.toLowerCase()
.replace(/^\[|\]$/g, '')
.replace(/\.$/, '');
if (url.protocol !== 'https:' || url.username || url.password || !hostname)
return null;
if (
hostname === 'localhost' ||
hostname.endsWith('.localhost') ||
hostname.endsWith('.local') ||
isNonGlobalIpv4(hostname) ||
hostname.includes(':')
)
return null;
url.hash = '';
return url.href;
} catch {
return null;
}
}
/** Normalize one Radio Browser station and omit favicons and unsafe streams. */
export function normalizeRadioBrowserStation(raw) {
const id = cleanRadioText(raw?.stationuuid, 40).toLowerCase();
const lat =
raw?.geo_lat === null || raw?.geo_lat === '' ? null : Number(raw?.geo_lat);
const lon =
raw?.geo_long === null || raw?.geo_long === ''
? null
: Number(raw?.geo_long);
const codec = cleanRadioText(raw?.codec, 16).toUpperCase();
const streamUrl = publicRadioHttpsUrl(raw?.url_resolved || raw?.url);
if (
!RADIO_UUID_RE.test(id) ||
Number(raw?.lastcheckok) !== 1 ||
Number(raw?.hls) === 1 ||
!Number.isFinite(lat) ||
lat < -90 ||
lat > 90 ||
!Number.isFinite(lon) ||
lon < -180 ||
lon > 180 ||
!/^(?:MP3|AAC(?:\+|-LC|-HE)?|HE-AAC)$/i.test(codec) ||
!streamUrl
)
return null;
const name = cleanRadioText(raw?.name, 140);
if (!name) return null;
const tags = String(raw?.tags ?? '')
.split(',')
.map((tag) =>
cleanRadioText(tag, 80)
.toLocaleLowerCase()
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim(),
)
.filter(Boolean)
.filter((tag, index, all) => all.indexOf(tag) === index)
.slice(0, 24);
const languages = String(raw?.language ?? '')
.split(',')
.map((language) => cleanRadioText(language, 40))
.filter(Boolean)
.slice(0, 8);
const rawCountryCode = cleanRadioText(raw?.countrycode, 2).toUpperCase();
const normalizedCode = normalizeRadioCountryInput(rawCountryCode);
const normalizedCountry =
normalizedCode.valid && !normalizedCode.empty
? normalizedCode
: normalizeRadioCountryInput(cleanRadioText(raw?.country, 80));
const bitrate = Number(raw?.bitrate);
return {
id,
name,
lat,
lon,
streamUrl,
homepage: publicRadioHttpsUrl(raw?.homepage),
tags,
languages,
state: cleanRadioText(raw?.state, 80),
country:
normalizedCountry.valid && !normalizedCountry.empty
? normalizedCountry.name
: cleanRadioText(raw?.country, 80),
countryCode: normalizedCountry.valid ? normalizedCountry.code : '',
metadataTrust: 'untrusted-community',
codec,
bitrate:
Number.isInteger(bitrate) && bitrate >= 8 && bitrate <= 1024
? bitrate
: null,
clickCount: Math.max(0, Math.min(10_000_000, Number(raw?.clickcount) || 0)),
};
}
export function publicRadioStation(station) {
return {
id: station.id,
name: station.name,
lat: station.lat,
lon: station.lon,
streamUrl: station.streamUrl,
homepage: station.homepage,
tags: station.tags,
languages: station.languages,
state: station.state,
country: station.country,
countryCode: station.countryCode,
metadataTrust: station.metadataTrust,
codec: station.codec,
bitrate: station.bitrate,
};
}
export {
cleanRadioText,
isNonGlobalIpv4,
publicRadioHttpsUrl,
normalizeRadioBrowserStation,
publicRadioStation,
} from '../../../src/sources/radioBrowser.js';