style: format code biome
This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+85
-67
@@ -9,88 +9,106 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Block SSRF — only allow http/https to public IPs (no private/loopback/link-local).
|
||||
const PRIVATE_IP_RE =
|
||||
/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|::1|fe80:|fc00:|fd00:|localhost)/i;
|
||||
/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|::1|fe80:|fc00:|fd00:|localhost)/i;
|
||||
|
||||
function isSafeUrl(url: string): boolean {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
||||
if (PRIVATE_IP_RE.test(u.hostname)) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const u = new URL(url);
|
||||
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
||||
if (PRIVATE_IP_RE.test(u.hostname)) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NowPlaying {
|
||||
title: string;
|
||||
artist: string | null;
|
||||
title: string;
|
||||
artist: string | null;
|
||||
}
|
||||
|
||||
async function fetchJson(url: string, ms = 4000): Promise<unknown> {
|
||||
if (!isSafeUrl(url)) return null;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), ms);
|
||||
try {
|
||||
const res = await fetch(url, { signal: controller.signal, cache: "no-store" });
|
||||
const text = await res.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text; // plain-text "Artist - Title" (Shoutcast/Icecast metadata)
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (!isSafeUrl(url)) return null;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), ms);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
const text = await res.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text; // plain-text "Artist - Title" (Shoutcast/Icecast metadata)
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function parseNowPlaying(d: unknown): NowPlaying | null {
|
||||
if (!d) return null;
|
||||
// AzureCast: { now_playing: { song: { title, artist } } }
|
||||
const azure = (d as { now_playing?: { song?: { title?: string; artist?: string; text?: string } } })
|
||||
.now_playing?.song;
|
||||
if (azure?.title || azure?.text) {
|
||||
return { title: azure.title ?? azure.text ?? "", artist: azure.artist ?? null };
|
||||
}
|
||||
// Generic JSON: { title, artist } or { songtitle }
|
||||
const generic = d as { title?: string; artist?: string; songtitle?: string };
|
||||
if (generic.title) return { title: generic.title, artist: generic.artist ?? null };
|
||||
if (generic.songtitle) {
|
||||
const [a, t] = generic.songtitle.split(" - ");
|
||||
return t ? { title: t.trim(), artist: a.trim() } : { title: generic.songtitle, artist: null };
|
||||
}
|
||||
// Plain text "Artist - Title"
|
||||
if (typeof d === "string" && d.trim()) {
|
||||
const parts = d.split(" - ");
|
||||
return parts.length > 1
|
||||
? { title: parts.slice(1).join(" - ").trim(), artist: parts[0].trim() }
|
||||
: { title: d.trim(), artist: null };
|
||||
}
|
||||
return null;
|
||||
if (!d) return null;
|
||||
// AzureCast: { now_playing: { song: { title, artist } } }
|
||||
const azure = (
|
||||
d as {
|
||||
now_playing?: {
|
||||
song?: { title?: string; artist?: string; text?: string };
|
||||
};
|
||||
}
|
||||
).now_playing?.song;
|
||||
if (azure?.title || azure?.text) {
|
||||
return {
|
||||
title: azure.title ?? azure.text ?? "",
|
||||
artist: azure.artist ?? null,
|
||||
};
|
||||
}
|
||||
// Generic JSON: { title, artist } or { songtitle }
|
||||
const generic = d as { title?: string; artist?: string; songtitle?: string };
|
||||
if (generic.title)
|
||||
return { title: generic.title, artist: generic.artist ?? null };
|
||||
if (generic.songtitle) {
|
||||
const [a, t] = generic.songtitle.split(" - ");
|
||||
return t
|
||||
? { title: t.trim(), artist: a.trim() }
|
||||
: { title: generic.songtitle, artist: null };
|
||||
}
|
||||
// Plain text "Artist - Title"
|
||||
if (typeof d === "string" && d.trim()) {
|
||||
const parts = d.split(" - ");
|
||||
return parts.length > 1
|
||||
? { title: parts.slice(1).join(" - ").trim(), artist: parts[0].trim() }
|
||||
: { title: d.trim(), artist: null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function fetchNowPlaying(): Promise<NowPlaying | null> {
|
||||
const url = (await siteSettings.get("radio_now_playing_api_url", "")) ?? "";
|
||||
if (!url) return null;
|
||||
return parseNowPlaying(await fetchJson(url));
|
||||
const url = (await siteSettings.get("radio_now_playing_api_url", "")) ?? "";
|
||||
if (!url) return null;
|
||||
return parseNowPlaying(await fetchJson(url));
|
||||
}
|
||||
|
||||
export async function fetchListeners(): Promise<number | null> {
|
||||
const url = (await siteSettings.get("radio_listeners_api_url", "")) ?? "";
|
||||
if (!url) return null;
|
||||
const d = await fetchJson(url);
|
||||
if (d == null) return null;
|
||||
const obj = d as { listeners?: unknown; current_listeners?: unknown };
|
||||
const raw =
|
||||
typeof d === "number"
|
||||
? d
|
||||
: (obj.listeners ?? obj.current_listeners ?? (typeof d === "string" ? Number(d) : null));
|
||||
const n = Number(
|
||||
typeof raw === "object" && raw && "total" in (raw as Record<string, unknown>)
|
||||
? (raw as { total: unknown }).total
|
||||
: raw,
|
||||
);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
const url = (await siteSettings.get("radio_listeners_api_url", "")) ?? "";
|
||||
if (!url) return null;
|
||||
const d = await fetchJson(url);
|
||||
if (d == null) return null;
|
||||
const obj = d as { listeners?: unknown; current_listeners?: unknown };
|
||||
const raw =
|
||||
typeof d === "number"
|
||||
? d
|
||||
: (obj.listeners ??
|
||||
obj.current_listeners ??
|
||||
(typeof d === "string" ? Number(d) : null));
|
||||
const n = Number(
|
||||
typeof raw === "object" &&
|
||||
raw &&
|
||||
"total" in (raw as Record<string, unknown>)
|
||||
? (raw as { total: unknown }).total
|
||||
: raw,
|
||||
);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
Reference in new issue
Block a user