← back to Allnewsdaily
scripts/check-live.js
135 lines
#!/usr/bin/env node
// Polls each outlet's YouTube /channel/<id>/live URL.
// YouTube returns the channel's currently-live broadcast page (200 + watch-page HTML)
// or a "no live stream" placeholder. We detect liveness by looking for the
// hlsManifestUrl / "isLive":true marker in the inline ytInitialPlayerResponse JSON.
//
// Writes data/live-status.json keyed by outlet.id:
// { "<id>": { isLive: bool, videoId: "...", checkedAt: ISO, error?: "..." } }
const fs = require('fs');
const path = require('path');
const https = require('https');
const OUTLETS_PATH = path.join(__dirname, '..', 'data', 'outlets.json');
const STATUS_PATH = path.join(__dirname, '..', 'data', 'live-status.json');
const UA =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36';
function fetchHtml(url, timeoutMs = 8000) {
return new Promise((resolve, reject) => {
const req = https.get(
url,
{
headers: {
'User-Agent': UA,
'Accept-Language': 'en-US,en;q=0.9',
Accept: 'text/html,application/xhtml+xml',
},
},
res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
// Follow one hop
fetchHtml(new URL(res.headers.location, url).toString(), timeoutMs).then(resolve, reject);
res.resume();
return;
}
if (res.statusCode !== 200) {
res.resume();
return reject(new Error('status ' + res.statusCode));
}
let body = '';
res.setEncoding('utf8');
res.on('data', d => {
body += d;
if (body.length > 4_000_000) {
req.destroy(new Error('body too big'));
}
});
res.on('end', () => resolve(body));
}
);
req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')));
req.on('error', reject);
});
}
function detectLive(html) {
// Marker 1 — hls manifest is present only when actively streaming
if (/"hlsManifestUrl":\s*"https:\/\/manifest\.googlevideo\.com/.test(html)) {
const m = html.match(/"videoId":\s*"([A-Za-z0-9_-]{11})"/);
return { isLive: true, videoId: m ? m[1] : null };
}
// Marker 2 — isLive:true inside videoDetails
if (/"videoDetails":\s*\{[^}]*"isLive":true/.test(html)) {
const m = html.match(/"videoId":\s*"([A-Za-z0-9_-]{11})"/);
return { isLive: true, videoId: m ? m[1] : null };
}
// Marker 3 — livestream microformat
if (/"liveBroadcastDetails":\s*\{[^}]*"isLiveNow":true/.test(html)) {
const m = html.match(/"videoId":\s*"([A-Za-z0-9_-]{11})"/);
return { isLive: true, videoId: m ? m[1] : null };
}
return { isLive: false, videoId: null };
}
async function checkOne(outlet) {
if (outlet.liveCheck !== 'youtube' || !outlet.youtube) {
return { id: outlet.id, isLive: false, videoId: null, skipped: true };
}
const url = `https://www.youtube.com/channel/${outlet.youtube}/live`;
try {
const html = await fetchHtml(url);
const { isLive, videoId } = detectLive(html);
return { id: outlet.id, isLive, videoId };
} catch (e) {
return { id: outlet.id, isLive: false, videoId: null, error: e.message };
}
}
async function runBatch(outlets, concurrency = 6) {
const results = {};
let idx = 0;
async function worker() {
while (idx < outlets.length) {
const o = outlets[idx++];
const r = await checkOne(o);
results[o.id] = {
isLive: r.isLive,
videoId: r.videoId,
checkedAt: new Date().toISOString(),
...(r.error ? { error: r.error } : {}),
};
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
}
async function main() {
const outlets = JSON.parse(fs.readFileSync(OUTLETS_PATH, 'utf8'));
const checkable = outlets.filter(o => o.liveCheck === 'youtube' && o.youtube);
console.log(`[check-live] checking ${checkable.length} of ${outlets.length} outlets`);
const t0 = Date.now();
const results = await runBatch(outlets);
const liveCount = Object.values(results).filter(r => r.isLive).length;
console.log(`[check-live] done in ${Date.now() - t0}ms — ${liveCount} live`);
// Merge with prior so we keep history if any outlet errored this tick
let prior = {};
try {
prior = JSON.parse(fs.readFileSync(STATUS_PATH, 'utf8'));
} catch (_) {}
const merged = { ...prior, ...results };
fs.writeFileSync(STATUS_PATH, JSON.stringify(merged, null, 2));
}
main().catch(e => {
console.error('[check-live] fatal', e);
process.exit(1);
});