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>
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
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);
|
||
}
|