diff --git a/apps/api-daemon/src/server.ts b/apps/api-daemon/src/server.ts index 4c151f8..18c6d4c 100644 --- a/apps/api-daemon/src/server.ts +++ b/apps/api-daemon/src/server.ts @@ -1,6 +1,7 @@ import express from "express"; import { prisma } from "@streamclipper/db"; import { env } from "./env"; +import { lastPollErrors } from "./youtubePolling"; export function startServer() { const app = express(); @@ -26,6 +27,7 @@ export function startServer() { lastCheckedAt: c.lastCheckedAt, recording: c.sessions.length > 0, activeSessionId: c.sessions[0]?.id ?? null, + lastPollError: lastPollErrors.get(c.channelId) ?? null, })), }); }); diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index bcc9452..6ad6c12 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -7,6 +7,14 @@ import { sendTelegramMessage } from "./telegram"; const execFileAsync = promisify(execFile); +/** + * yt-dlp exits non-zero both when a channel isn't live and when the check + * itself fails (network block, bot detection, extractor breakage). Those two + * cases must not be indistinguishable, so the last failure per channel is + * kept here and surfaced via GET /status for debugging. + */ +export const lastPollErrors = new Map(); + /** * Live detection goes through yt-dlp itself instead of the YouTube Data API. * `search.list?eventType=live` costs 100 quota units per call against a @@ -23,10 +31,12 @@ async function findLiveVideoId(channelId: string): Promise { ["--simulate", "--no-warnings", "--print", "%(id)s", liveUrl], { timeout: 20_000 }, ); + lastPollErrors.delete(channelId); const videoId = stdout.trim().split("\n")[0]; return videoId || null; - } catch { - // yt-dlp exits non-zero when the channel isn't currently live + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + lastPollErrors.set(channelId, message.slice(0, 500)); return null; } }