import { randomUUID } from "node:crypto"; import path from "node:path"; import { logger } from "../logger.js"; import type { Requester, Track } from "../types.js"; const log = logger.child({ mod: "direct" }); const AUDIO_CONTENT_TYPES = [ "audio/", "application/ogg", "application/x-mpegurl", "application/vnd.apple.mpegurl", "video/mp4", "video/webm", ]; export interface ProbeResult { isMedia: boolean; isLive: boolean; title: string | null; } /** Cheap HEAD probe used to tell "direct media URL" apart from "web page". */ export async function probe(url: string): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 8000); try { const res = await fetch(url, { method: "HEAD", redirect: "follow", signal: controller.signal, headers: { "user-agent": "stoat-mbot/0.1", icy: "1" }, }); const contentType = (res.headers.get("content-type") ?? "").toLowerCase(); const isMedia = AUDIO_CONTENT_TYPES.some((type) => contentType.startsWith(type)); // Shoutcast/Icecast expose the station name and never a content length. const icyName = res.headers.get("icy-name"); const isLive = isMedia && !res.headers.get("content-length"); return { isMedia: isMedia || Boolean(icyName), isLive: isLive || Boolean(icyName), title: icyName }; } catch (err) { log.debug({ err, url }, "probe failed"); return { isMedia: false, isLive: false, title: null }; } finally { clearTimeout(timer); } } export function toTrack(url: string, requestedBy: Requester, probed: ProbeResult): Track { let title = probed.title; if (!title) { try { const name = path.basename(new URL(url).pathname); title = decodeURIComponent(name) || new URL(url).hostname; } catch { title = url; } } let host: string | null = null; try { host = new URL(url).hostname; } catch { host = null; } return { id: randomUUID(), title, author: host, duration: 0, isLive: probed.isLive, url, thumbnail: null, source: "direct", requestedBy, }; }