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:
@@ -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
|
* solver) is included too — some channels return "The page needs to be
|
||||||
* reloaded" on the plain metadata fetch too, not just format resolution.
|
* 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`;
|
const liveUrl = `https://www.youtube.com/channel/${channelId}/live`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +66,12 @@ async function findLiveVideoId(channelId: string): Promise<string | null> {
|
|||||||
],
|
],
|
||||||
{ timeout: 20_000 },
|
{ timeout: 20_000 },
|
||||||
);
|
);
|
||||||
|
// 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);
|
lastPollErrors.delete(channelId);
|
||||||
|
await notifyIfEnabled("notify_poll_error", `✅ *${channelName}* canlı-tespiti tekrar çalışıyor.`);
|
||||||
|
}
|
||||||
const videoId = stdout.trim().split("\n")[0];
|
const videoId = stdout.trim().split("\n")[0];
|
||||||
return videoId || null;
|
return videoId || null;
|
||||||
} catch (err) {
|
} 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
|
// yt-dlp reports "not live" / "starts in N minutes" as a non-zero exit
|
||||||
// too — normal, expected outcomes, not failures worth a red badge.
|
// too — normal, expected outcomes, not failures worth a red badge.
|
||||||
if (!BENIGN_POLL_MESSAGE.test(message)) {
|
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() });
|
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 {
|
} else {
|
||||||
lastPollErrors.delete(channelId);
|
lastPollErrors.delete(channelId);
|
||||||
}
|
}
|
||||||
@@ -105,10 +124,7 @@ export async function startCaptureSession(
|
|||||||
segmentTimeSec: channel.segmentTimeSec ?? undefined,
|
segmentTimeSec: channel.segmentTimeSec ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notifySetting = await prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } });
|
await notifyIfEnabled("notify_stream_start", `🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`);
|
||||||
if (notifySetting?.value !== "false") {
|
|
||||||
await sendTelegramMessage(`🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`);
|
|
||||||
}
|
|
||||||
console.log(`[youtube-polling] started session ${session.id} for channel ${channel.name}`);
|
console.log(`[youtube-polling] started session ${session.id} for channel ${channel.name}`);
|
||||||
return { started: true };
|
return { started: true };
|
||||||
}
|
}
|
||||||
@@ -123,7 +139,7 @@ export async function checkChannel(channel: {
|
|||||||
liveVideoId: string | null;
|
liveVideoId: string | null;
|
||||||
error: PollError | null;
|
error: PollError | null;
|
||||||
}> {
|
}> {
|
||||||
const liveVideoId = await findLiveVideoId(channel.channelId);
|
const liveVideoId = await findLiveVideoId(channel.channelId, channel.name);
|
||||||
await prisma.channel.update({
|
await prisma.channel.update({
|
||||||
where: { id: channel.id },
|
where: { id: channel.id },
|
||||||
data: { lastCheckedAt: new Date() },
|
data: { lastCheckedAt: new Date() },
|
||||||
|
|||||||
@@ -158,7 +158,13 @@ export async function updateSttEnabled(formData: FormData) {
|
|||||||
export async function updateNotificationPrefs(formData: FormData) {
|
export async function updateNotificationPrefs(formData: FormData) {
|
||||||
const streamStart = formData.get("notifyStreamStart") === "true";
|
const streamStart = formData.get("notifyStreamStart") === "true";
|
||||||
const renderDone = formData.get("notifyRenderDone") === "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({
|
await prisma.appSetting.upsert({
|
||||||
where: { key: "notify_stream_start" },
|
where: { key: "notify_stream_start" },
|
||||||
create: { key: "notify_stream_start", value: String(streamStart) },
|
create: { key: "notify_stream_start", value: String(streamStart) },
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ export const dynamic = "force-dynamic";
|
|||||||
const STALE_COOKIE_HOURS = 4;
|
const STALE_COOKIE_HOURS = 4;
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
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: "ytdlp_cookies" } }),
|
||||||
prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }),
|
prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }),
|
||||||
prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }),
|
prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }),
|
||||||
prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }),
|
prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }),
|
||||||
prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }),
|
prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }),
|
||||||
|
prisma.appSetting.findUnique({ where: { key: "notify_poll_error" } }),
|
||||||
prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }),
|
prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export default async function SettingsPage() {
|
|||||||
const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24;
|
const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24;
|
||||||
const notifyStreamStart = notifyStart?.value !== "false";
|
const notifyStreamStart = notifyStart?.value !== "false";
|
||||||
const notifyRenderDone = notifyRender?.value !== "false";
|
const notifyRenderDone = notifyRender?.value !== "false";
|
||||||
|
const notifyPollError = notifyError?.value !== "false";
|
||||||
const sttEnabled = sttSetting?.value !== "false";
|
const sttEnabled = sttSetting?.value !== "false";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -126,6 +128,10 @@ export default async function SettingsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<form action={updateNotificationPrefs}>
|
<form action={updateNotificationPrefs}>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
|
||||||
|
Canlı-tespit hata verince/düzelince Telegram bildirimi
|
||||||
|
<SettingSwitch name="notifyPollError" defaultChecked={notifyPollError} />
|
||||||
|
</Label>
|
||||||
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
|
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
|
||||||
Yayın başladığında Telegram bildirimi
|
Yayın başladığında Telegram bildirimi
|
||||||
<SettingSwitch name="notifyStreamStart" defaultChecked={notifyStreamStart} />
|
<SettingSwitch name="notifyStreamStart" defaultChecked={notifyStreamStart} />
|
||||||
|
|||||||
Reference in New Issue
Block a user