From a53f0a5e0499812ff5466906db18cda2f75a57a7 Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Wed, 2 Sep 2026 20:53:56 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20canl=C4=B1-tespit=20hatas=C4=B1=20art?= =?UTF-8?q?=C4=B1k=20Telegram'a=20d=C3=BC=C5=9F=C3=BCyor=20+=20zaman=20dam?= =?UTF-8?q?gas=C4=B1=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Poll hatası şu ana kadar sadece panele girip bakınca görülüyordu — proaktif haberdar olma yolu yoktu. Şimdi bir kanal hata vermeye BAŞLADIĞINDA (her 60sn'de tekrar değil, sadece durum değişince) ve düzeldiğinde Telegram bildirimi gidiyor. Ayarlar'dan kapatılabilir (notify_poll_error). Co-Authored-By: Claude Sonnet 5 --- apps/api-daemon/src/youtubePolling.ts | 30 ++++++++++++++++++++------- apps/frontend/app/actions.ts | 6 ++++++ apps/frontend/app/settings/page.tsx | 8 ++++++- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index 85f7ce4..4c0434f 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -43,7 +43,12 @@ 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 findLiveVideoId(channelId: string): Promise { +async function notifyIfEnabled(key: string, text: string): Promise { + const setting = await prisma.appSetting.findUnique({ where: { key } }); + if (setting?.value !== "false") await sendTelegramMessage(text); +} + +async function findLiveVideoId(channelId: string, channelName: string): Promise { const liveUrl = `https://www.youtube.com/channel/${channelId}/live`; try { @@ -61,7 +66,12 @@ async function findLiveVideoId(channelId: string): Promise { ], { timeout: 20_000 }, ); - lastPollErrors.delete(channelId); + // Only worth a "recovered" message if it was actually failing before — + // 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.`); + } const videoId = stdout.trim().split("\n")[0]; return videoId || null; } catch (err) { @@ -69,7 +79,16 @@ async function findLiveVideoId(channelId: string): Promise { // yt-dlp reports "not live" / "starts in N minutes" as a non-zero exit // too — normal, expected outcomes, not failures worth a red badge. if (!BENIGN_POLL_MESSAGE.test(message)) { + // Alert only on the transition into failing — not every 60s poll + // while it stays broken, or this would spam constantly. + const wasAlreadyFailing = lastPollErrors.has(channelId); lastPollErrors.set(channelId, { message: message.slice(0, 500), at: new Date().toISOString() }); + if (!wasAlreadyFailing) { + await notifyIfEnabled( + "notify_poll_error", + `⚠️ *${channelName}* canlı-tespiti başarısız oluyor:\n\`${message.slice(0, 300)}\``, + ); + } } else { lastPollErrors.delete(channelId); } @@ -105,10 +124,7 @@ export async function startCaptureSession( segmentTimeSec: channel.segmentTimeSec ?? undefined, }); - const notifySetting = await prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }); - if (notifySetting?.value !== "false") { - await sendTelegramMessage(`🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`); - } + await notifyIfEnabled("notify_stream_start", `🔴 *${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 }; } @@ -123,7 +139,7 @@ export async function checkChannel(channel: { liveVideoId: string | null; error: PollError | null; }> { - const liveVideoId = await findLiveVideoId(channel.channelId); + const liveVideoId = await findLiveVideoId(channel.channelId, channel.name); await prisma.channel.update({ where: { id: channel.id }, data: { lastCheckedAt: new Date() }, diff --git a/apps/frontend/app/actions.ts b/apps/frontend/app/actions.ts index 9026772..779a555 100644 --- a/apps/frontend/app/actions.ts +++ b/apps/frontend/app/actions.ts @@ -158,7 +158,13 @@ export async function updateSttEnabled(formData: FormData) { export async function updateNotificationPrefs(formData: FormData) { const streamStart = formData.get("notifyStreamStart") === "true"; const renderDone = formData.get("notifyRenderDone") === "true"; + const pollError = formData.get("notifyPollError") === "true"; + await prisma.appSetting.upsert({ + where: { key: "notify_poll_error" }, + create: { key: "notify_poll_error", value: String(pollError) }, + update: { value: String(pollError) }, + }); await prisma.appSetting.upsert({ where: { key: "notify_stream_start" }, create: { key: "notify_stream_start", value: String(streamStart) }, diff --git a/apps/frontend/app/settings/page.tsx b/apps/frontend/app/settings/page.tsx index c027de4..206dfff 100644 --- a/apps/frontend/app/settings/page.tsx +++ b/apps/frontend/app/settings/page.tsx @@ -14,12 +14,13 @@ export const dynamic = "force-dynamic"; const STALE_COOKIE_HOURS = 4; export default async function SettingsPage() { - const [setting, pollSetting, ttlSetting, notifyStart, notifyRender, sttSetting] = await Promise.all([ + const [setting, pollSetting, ttlSetting, notifyStart, notifyRender, notifyError, sttSetting] = await Promise.all([ prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } }), prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }), prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }), prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }), prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }), + prisma.appSetting.findUnique({ where: { key: "notify_poll_error" } }), prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }), ]); @@ -29,6 +30,7 @@ export default async function SettingsPage() { const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24; const notifyStreamStart = notifyStart?.value !== "false"; const notifyRenderDone = notifyRender?.value !== "false"; + const notifyPollError = notifyError?.value !== "false"; const sttEnabled = sttSetting?.value !== "false"; return ( @@ -126,6 +128,10 @@ export default async function SettingsPage() {
+