Files
screenclipper/apps/api-daemon/src/youtubePolling.ts
T
ayrisdevandClaude Sonnet 5 d05aed72ff fix: canlı-tespitini yt-dlp'ye geri taşı — HTML regex yanlış videoId yakalıyordu
Kullanıcı panelindeki kayıtların kendi yayınıyla alakasız olduğunu
bildirdi. Sebep: channel /live sayfası onlarca alakasız videoId
içeriyor (kanal video listesi, öneriler); düz regex sayfadaki İLK
videoId'yi alıyordu, bu da rastgele başka bir videoydu — isLive:true
kontrolü de aynı bağlamda değildi. yt-dlp'nin extractor'ı kanalın asıl
o anki canlı videosunu doğru çözüyor; -f (format) verilmediği için
PO-token duvarını da tetiklemiyor (format çözümleme sadece gerçek
capture adımında, -f ile, gerekiyor).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 18:46:59 +03:00

94 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { prisma } from "@streamclipper/db";
import { env } from "./env";
import { streamIngestQueue } from "./queues";
import { sendTelegramMessage } from "./telegram";
import { ytdlpAntiBotArgs } from "./ytdlpCookies";
const execFileAsync = promisify(execFile);
/**
* Live detection failures are kept per channel and surfaced via GET /status
* for debugging.
*/
export const lastPollErrors = new Map<string, string>();
/**
* A channel's /live page HTML embeds dozens of unrelated `videoId`s (channel
* video list, recommendations, etc.) — a plain regex grabbed whichever one
* happened to appear first, which had nothing to do with the actual live
* broadcast (confirmed: captured recordings didn't match the real stream).
* yt-dlp's extractor correctly resolves the channel's *current* live video,
* so the check goes back through it. Crucially, no `-f` format selector is
* passed here: format resolution is what triggers YouTube's PO-token wall,
* and this call only needs the video id / live flag, not downloadable
* formats — those are only resolved later, in the actual capture.
*/
async function findLiveVideoId(channelId: string): Promise<string | null> {
const liveUrl = `https://www.youtube.com/channel/${channelId}/live`;
try {
const { stdout } = await execFileAsync(
"yt-dlp",
["--simulate", "--no-warnings", ...ytdlpAntiBotArgs(), "--print", "%(id)s", liveUrl],
{ timeout: 20_000 },
);
lastPollErrors.delete(channelId);
const videoId = stdout.trim().split("\n")[0];
return videoId || null;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
lastPollErrors.set(channelId, message.slice(0, 500));
return null;
}
}
async function startSession(channel: { id: string; name: string }, liveVideoId: string) {
const existing = await prisma.streamSession.findFirst({
where: { channelId: channel.id, endedAt: null },
});
if (existing) return;
const session = await prisma.streamSession.create({
data: { channelId: channel.id, liveVideoId },
});
await streamIngestQueue.add("start-capture", {
sessionId: session.id,
channelDbId: channel.id,
youtubeUrl: `https://www.youtube.com/watch?v=${liveVideoId}`,
});
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}`);
}
export async function pollOnce(): Promise<void> {
const channels = await prisma.channel.findMany({ where: { isActive: true } });
for (const channel of channels) {
try {
const liveVideoId = await findLiveVideoId(channel.channelId);
await prisma.channel.update({
where: { id: channel.id },
data: { lastCheckedAt: new Date() },
});
if (liveVideoId) {
await startSession(channel, liveVideoId);
}
} catch (err) {
console.error(`[youtube-polling] error polling channel ${channel.name}:`, err);
}
}
}
export function startPollingLoop(): NodeJS.Timeout {
console.log(`[youtube-polling] polling every ${env.pollIntervalMs}ms`);
pollOnce().catch((err) => console.error("[youtube-polling] initial poll failed:", err));
return setInterval(() => {
pollOnce().catch((err) => console.error("[youtube-polling] poll failed:", err));
}, env.pollIntervalMs);
}