// Re-check the video sources that are actually live (Marty, 2026-09-24). // // videoCheck runs when a campaign is SAVED, which leaves two holes it can never cover: // // 1. anything created before the check existed. Campaign #146 pointed at a placeholder, // https://yourdomain.com/..., from 16 September. The check shipped on the 22nd. It sat // live for another two days handing every viewer a black player and burning one of their // daily video slots, until Marty hit it himself and asked why nothing played. // 2. link rot. A source that was fine on the day can 404 a week later, and nothing notices. // // So the live ones get re-checked on a schedule. A source that fails is PAUSED, never deleted: // the advertiser keeps every unspent credit (reservation is computed, not deducted) and can fix // the link and start it again. Failing is reported, not silent, because a check that quietly // does nothing is how the first one went unnoticed. 'use strict'; let X = null; const STATE = () => X.path.join(X.dataDir, 'videosweep.json'); function load() { try { return JSON.parse(X.fs.readFileSync(STATE(), 'utf8')); } catch (e) { return { checked: {}, at: 0 }; } } function save(d) { try { X.fs.writeFileSync(STATE(), JSON.stringify(d)); } catch (e) {} } function init(deps) { X = deps; } // One pass. Returns what it found so the admin route and the scheduler can both report it. async function run(opts) { const dry = !!(opts && opts.dry); if (!X || !X.db || !X.db.enabled()) return { error: 'no database' }; const rows = await X.db.q( "SELECT id, owner_email, name, image_url, status FROM campaigns WHERE type='video' AND status IN ('active','paused')"); const out = { checked: 0, ok: 0, broken: [], paused: [], skipped: 0 }; for (const c of rows) { const url = String(c.image_url || '').trim(); if (!url) { out.skipped++; continue; } if (/^\/uploads\//.test(url)) { out.skipped++; continue; } // on our own disk out.checked++; let v; try { v = await X.videoCheck(url); } catch (e) { v = { ok: false, reason: 'check threw: ' + e.message }; } if (v.ok) { out.ok++; continue; } out.broken.push({ id: c.id, name: c.name, owner: c.owner_email, url, reason: v.reason, status: c.status }); // only an ACTIVE one needs stopping; a paused one is already out of rotation if (c.status === 'active' && !dry) { try { await X.db.q("UPDATE campaigns SET status='paused' WHERE id=?", [c.id]); out.paused.push(c.id); } catch (e) { // never swallow this: a guard that fails quietly is the whole reason #146 survived out.failed = out.failed || []; out.failed.push({ id: c.id, error: e.message }); console.error('videosweep could not pause #' + c.id + ': ' + e.message); } } } if (!dry) { const st = load(); st.at = Date.now(); st.lastBroken = out.broken.map(b => b.id); save(st); if (out.paused.length) { const lines = out.broken.filter(b => out.paused.includes(b.id)) .map(b => '#' + b.id + ' "' + b.name + '" (' + b.owner + ')\n ' + b.url); console.log('videosweep paused ' + out.paused.length + ' broken video campaign(s): ' + out.paused.join(', ')); if (X.alert) { X.alert('\u{1F6D1} Video sources that no longer load\n\n' + lines.join('\n') + '\n\nPaused so members stop losing a daily video slot on a black player. ' + 'Every unspent credit stays with the advertiser and the campaign can be restarted once the link is fixed.').catch(() => {}); } } } return out; } // Daily is plenty: a source that dies is a slow problem, and HEADing every live video more often // than that is noise for the advertisers' servers. function start() { const tick = () => { run({}).catch(e => console.error('videosweep', e.message)); }; setTimeout(tick, 90000); // once, shortly after boot setInterval(tick, 24 * 60 * 60 * 1000); } module.exports = { init, run, start };