feat: canlı-tespit hatası artık Telegram'a düşüyor + zaman damgası ekle

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 20:53:56 +03:00
co-authored by Claude Sonnet 5
parent ce9e01bbeb
commit a53f0a5e04
3 changed files with 36 additions and 8 deletions
+23 -7
View File
@@ -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<string | null> {
async function notifyIfEnabled(key: string, text: string): Promise<void> {
const setting = await prisma.appSetting.findUnique({ where: { key } });
if (setting?.value !== "false") await sendTelegramMessage(text);
}
async function findLiveVideoId(channelId: string, channelName: string): Promise<string | null> {
const liveUrl = `https://www.youtube.com/channel/${channelId}/live`;
try {
@@ -61,7 +66,12 @@ async function findLiveVideoId(channelId: string): Promise<string | null> {
],
{ 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<string | null> {
// 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() },