From b62d6a7e5fe16839354ed35bbb6bc5aca5079b8c Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Sun, 30 Aug 2026 17:31:29 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20canl=C4=B1-tespitini=20yt-dlp'den=20d?= =?UTF-8?q?=C3=BCz=20HTTP=20fetch'e=20ta=C5=9F=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PO-token duvarı polling'de hiç gerekli değildi — sadece stream byte'ı indirirken (capture) lazım. yt-dlp --simulate bile format çözümlemeye çalıştığı için PO-token'a takılıyordu. Artık /channel//live sayfasını düz fetch ile çekip ytInitialData içindeki isLive/videoId'yi regex ile okuyoruz — bot-check/cookie/PO-token'a hiç maruz kalmıyor. yt-dlp + cookie + pot-provider sadece gerçek capture adımında kalıyor. Co-Authored-By: Claude Sonnet 5 --- apps/api-daemon/src/youtubePolling.ts | 52 +++++++++++++-------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index 083fbfc..dae1252 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -1,47 +1,45 @@ -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); +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"; /** - * yt-dlp exits non-zero both when a channel isn't live and when the check - * itself fails (network block, bot detection, extractor breakage). Those two - * cases must not be indistinguishable, so the last failure per channel is - * kept here and surfaced via GET /status for debugging. + * Live detection failures are kept per channel and surfaced via GET /status + * for debugging. */ export const lastPollErrors = new Map(); /** - * Live detection goes through yt-dlp itself instead of the YouTube Data API. - * `search.list?eventType=live` costs 100 quota units per call against a - * 10,000/day free quota — polling one channel every 60s alone would need - * ~144,000 units/day, well over quota. yt-dlp's `/live` redirect check is - * free and needs no API key. + * 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. */ async function findLiveVideoId(channelId: string): Promise { const liveUrl = `https://www.youtube.com/channel/${channelId}/live`; try { - const { stdout } = await execFileAsync( - "yt-dlp", - [ - "--simulate", - "--no-warnings", - ...ytdlpAntiBotArgs(), - "-f", "bestvideo+bestaudio/best", - "--print", "%(id)s", - liveUrl, - ], - { timeout: 20_000 }, - ); + 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(); lastPollErrors.delete(channelId); - const videoId = stdout.trim().split("\n")[0]; - return videoId || null; + + if (!/"isLive":true/.test(html)) { + return null; + } + + const match = html.match(/"videoId":"([^"]+)"/); + return match ? match[1] : null; } catch (err) { const message = err instanceof Error ? err.message : String(err); lastPollErrors.set(channelId, message.slice(0, 500));