refactor: extract CCTV media and radio directory providers

This commit is contained in:
Sameh Khamis committed 2026-09-12 12:09:02 -07:00
1 parent fada0c661f
commit 79b4929b88
17 files changed
+1977 -1831

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
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,
};
}