mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor: expose portable source record protocols
This commit is contained in:
1 parent
6fa25c349a
commit
ea6c8653b3
12 files changed
+370
-192
No files matched your search
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
Reference in new issue
Block a user