diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index 087ec7d..c59bc65 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -43,12 +43,29 @@ const BENIGN_POLL_MESSAGE = /is not currently live|will begin in/; * solver) is included too — some channels return "The page needs to be * reloaded" on the plain metadata fetch too, not just format resolution. */ -async function notifyIfEnabled(key: string, text: string): Promise { +/** + * A channel whose capture keeps crashing and restarting (see streamIngest's + * crash-loop notes) fires "started" + "poll failing" + "poll recovered" over + * and over within minutes — observed directly once several channels were + * live at once. One shared per-channel cooldown across all three message + * types means a flapping channel says its piece once, then goes quiet for a + * while instead of narrating every blip. + */ +const NOTIFY_COOLDOWN_MS = 10 * 60 * 1000; +const lastNotifyAt = new Map(); + +async function notifyIfEnabled(key: string, channelId: string, text: string): Promise { + const last = lastNotifyAt.get(channelId) ?? 0; + if (Date.now() - last < NOTIFY_COOLDOWN_MS) return; + const setting = await prisma.appSetting.findUnique({ where: { key } }); - if (setting?.value !== "false") await sendTelegramMessage(text); + if (setting?.value === "false") return; + + lastNotifyAt.set(channelId, Date.now()); + await sendTelegramMessage(text); } -async function findLiveVideoId(channelId: string, channelName: string): Promise { +async function findLiveVideoId(channelId: string, channelName: string, channelDbId: string): Promise { const liveUrl = `https://www.youtube.com/channel/${channelId}/live`; try { @@ -70,7 +87,7 @@ async function findLiveVideoId(channelId: string, channelName: string): Promise< // this runs on every successful poll, most of which were never broken. if (lastPollErrors.has(channelId)) { lastPollErrors.delete(channelId); - await notifyIfEnabled("notify_poll_error", `✅ *${channelName}* canlı-tespiti tekrar çalışıyor.`); + await notifyIfEnabled("notify_poll_error", channelDbId, `✅ *${channelName}* canlı-tespiti tekrar çalışıyor.`); } const videoId = stdout.trim().split("\n")[0]; return videoId || null; @@ -86,6 +103,7 @@ async function findLiveVideoId(channelId: string, channelName: string): Promise< if (!wasAlreadyFailing) { await notifyIfEnabled( "notify_poll_error", + channelDbId, `⚠️ *${channelName}* canlı-tespiti başarısız oluyor:\n\`${message.slice(0, 300)}\``, ); } @@ -124,7 +142,7 @@ export async function startCaptureSession( segmentTimeSec: channel.segmentTimeSec ?? undefined, }); - await notifyIfEnabled("notify_stream_start", `🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`); + await notifyIfEnabled("notify_stream_start", channel.id, `🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`); console.log(`[youtube-polling] started session ${session.id} for channel ${channel.name}`); return { started: true }; } @@ -139,7 +157,7 @@ export async function checkChannel(channel: { liveVideoId: string | null; error: PollError | null; }> { - const liveVideoId = await findLiveVideoId(channel.channelId, channel.name); + const liveVideoId = await findLiveVideoId(channel.channelId, channel.name, channel.id); await prisma.channel.update({ where: { id: channel.id }, data: { lastCheckedAt: new Date() },