fix: canlı-tespitini yt-dlp'siz, doğru kapsamlı fetch'e taşı
Proxy'ye gerek kalmadan bir çözüm: bugün boyunca düz HTTP fetch hiç engellenmedi, sadece yt-dlp çağrıları bot-check/PO-token duvarına takıldı. Önceki naive regex sorunuysa "videoId" alanının sayfada onlarca kez (kanal video listesi, öneriler) geçmesiydi. Çözüm: YouTube'un videoDetails objesi — sayfa gerçekten canlıysa tek ve biricik olarak videoId/isLive/channelId'yi birlikte içeriyor (canlı değilse obje hiç yok, doğrulandı). channelId çapraz kontrolüyle birlikte hem doğru video hem yt-dlp'ye hiç dokunmadan (dolayısıyla bot-check'e hiç maruz kalmadan) tespit yapılıyor. yt-dlp + cookie + pot-provider artık sadece gerçek capture adımında kullanılıyor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { prisma } from "@streamclipper/db";
|
import { prisma } from "@streamclipper/db";
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
import { streamIngestQueue } from "./queues";
|
import { streamIngestQueue } from "./queues";
|
||||||
import { sendTelegramMessage } from "./telegram";
|
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live detection failures are kept per channel and surfaced via GET /status
|
* Live detection failures are kept per channel and surfaced via GET /status
|
||||||
@@ -15,28 +13,45 @@ const execFileAsync = promisify(execFile);
|
|||||||
export const lastPollErrors = new Map<string, string>();
|
export const lastPollErrors = new Map<string, string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A channel's /live page HTML embeds dozens of unrelated `videoId`s (channel
|
* Two earlier approaches both had real problems:
|
||||||
* video list, recommendations, etc.) — a plain regex grabbed whichever one
|
* - A naive regex over the channel /live page HTML grabbed the FIRST
|
||||||
* happened to appear first, which had nothing to do with the actual live
|
* "videoId" anywhere on the page — which is one of dozens of unrelated
|
||||||
* broadcast (confirmed: captured recordings didn't match the real stream).
|
* ids (channel video list, recommendations), not the live broadcast.
|
||||||
* yt-dlp's extractor correctly resolves the channel's *current* live video,
|
* Confirmed: captured recordings didn't match the real stream.
|
||||||
* so the check goes back through it. Crucially, no `-f` format selector is
|
* - Routing the check through yt-dlp resolved the correct video but is
|
||||||
* passed here: format resolution is what triggers YouTube's PO-token wall,
|
* increasingly blocked by YouTube's bot-check/proof-of-origin-token wall
|
||||||
* and this call only needs the video id / live flag, not downloadable
|
* on datacenter IPs — even a plain --simulate call, no cookies fix this
|
||||||
* formats — those are only resolved later, in the actual capture.
|
* reliably.
|
||||||
|
*
|
||||||
|
* A plain page fetch was never blocked all day — only yt-dlp invocations
|
||||||
|
* were. The fix is extracting the *right* field instead of avoiding the
|
||||||
|
* fetch: YouTube embeds a single `videoDetails` object holding the id/live
|
||||||
|
* flag/channelId of whichever video the page is actually showing (present
|
||||||
|
* only when a channel really is live — verified empty for an offline
|
||||||
|
* channel). Scoping to that one object (and cross-checking channelId)
|
||||||
|
* avoids both the wrong-video bug and yt-dlp's bot-check entirely.
|
||||||
*/
|
*/
|
||||||
async function findLiveVideoId(channelId: string): Promise<string | null> {
|
async function findLiveVideoId(channelId: string): Promise<string | null> {
|
||||||
const liveUrl = `https://www.youtube.com/channel/${channelId}/live`;
|
const liveUrl = `https://www.youtube.com/channel/${channelId}/live`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const res = await fetch(liveUrl, { headers: { "User-Agent": BROWSER_USER_AGENT } });
|
||||||
"yt-dlp",
|
if (!res.ok) {
|
||||||
["--simulate", "--no-warnings", ...(await ytdlpAntiBotArgs()), "--print", "%(id)s", liveUrl],
|
lastPollErrors.set(channelId, `HTTP ${res.status} fetching ${liveUrl}`);
|
||||||
{ timeout: 20_000 },
|
return null;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const html = await res.text();
|
||||||
lastPollErrors.delete(channelId);
|
lastPollErrors.delete(channelId);
|
||||||
const videoId = stdout.trim().split("\n")[0];
|
|
||||||
return videoId || null;
|
const match = html.match(/"videoDetails":\{"videoId":"([^"]+)"/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const window = html.slice(match.index!, match.index! + 600);
|
||||||
|
if (!/"isLive":true/.test(window)) return null;
|
||||||
|
if (!window.includes(`"channelId":"${channelId}"`)) return null;
|
||||||
|
|
||||||
|
return match[1];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
lastPollErrors.set(channelId, message.slice(0, 500));
|
lastPollErrors.set(channelId, message.slice(0, 500));
|
||||||
|
|||||||
Reference in New Issue
Block a user