From d05aed72ff46c7ccbd1890f39fb6477278646195 Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Sun, 30 Aug 2026 18:46:59 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20canl=C4=B1-tespitini=20yt-dlp'ye=20geri?= =?UTF-8?q?=20ta=C5=9F=C4=B1=20=E2=80=94=20HTML=20regex=20yanl=C4=B1=C5=9F?= =?UTF-8?q?=20videoId=20yakal=C4=B1yordu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api-daemon/src/youtubePolling.ts | 43 +++++++++++++-------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index dae1252..86e1d73 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -1,10 +1,12 @@ +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 BROWSER_USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"; +const execFileAsync = promisify(execFile); /** * Live detection failures are kept per channel and surfaced via GET /status @@ -13,33 +15,28 @@ const BROWSER_USER_AGENT = export const lastPollErrors = new Map(); /** - * Live detection is a plain page fetch, not a yt-dlp invocation. Resolving - * downloadable formats (what yt-dlp does even in --simulate mode) is what - * triggers YouTube's bot-check and proof-of-origin token requirements — but - * we don't need stream bytes here, just whether the channel is live, which - * is embedded directly in the channel's /live page HTML (`isLive`/`videoId` - * in ytInitialData). This avoids cookies/PO-token entirely for polling; - * those are only needed later, for the actual stream-ingest capture. + * 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 { const liveUrl = `https://www.youtube.com/channel/${channelId}/live`; try { - const res = await fetch(liveUrl, { headers: { "User-Agent": BROWSER_USER_AGENT } }); - if (!res.ok) { - lastPollErrors.set(channelId, `HTTP ${res.status} fetching ${liveUrl}`); - return null; - } - - const html = await res.text(); + const { stdout } = await execFileAsync( + "yt-dlp", + ["--simulate", "--no-warnings", ...ytdlpAntiBotArgs(), "--print", "%(id)s", liveUrl], + { timeout: 20_000 }, + ); lastPollErrors.delete(channelId); - - if (!/"isLive":true/.test(html)) { - return null; - } - - const match = html.match(/"videoId":"([^"]+)"/); - return match ? match[1] : null; + 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));