Compare commits

...
2 Commits
Author SHA1 Message Date
ayrisdevandClaude Sonnet 5 eca80959fb fix: aday klibi olmayan segmentlerde neden hiçbir şey olmadığı belirsizdi
UI yenilemesinde segments/page.tsx'teki "aday klip bulunamadı" boş-durum
mesajı kanal detay sayfasına taşınmamıştı — sinyal analizi bir segmentte
ilginç an bulamayınca panelde sessizce hiçbir şey görünmüyordu, bozukmuş
gibi duruyordu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 21:06:08 +03:00
ayrisdevandClaude Sonnet 5 a53f0a5e04 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>
2026-09-02 20:53:56 +03:00
4 changed files with 42 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 * 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 },
); );
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]; 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() },
+6
View File
@@ -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) },
+6
View File
@@ -235,6 +235,12 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
</form> </form>
</div> </div>
{segment.candidates.length === 0 && segment.status !== "PENDING" && (
<p className="mt-2 text-xs text-muted-foreground">
Bu segmentte aday klip bulunamadı (sinyal analizi ilginç bir an tespit etmedi).
</p>
)}
{segment.candidates.map((c) => ( {segment.candidates.map((c) => (
<div key={c.id} className="mt-3 border-l-2 border-border pl-4"> <div key={c.id} className="mt-3 border-l-2 border-border pl-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
+7 -1
View File
@@ -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} />