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>
This commit is contained in:
@@ -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<string, string>();
|
||||
|
||||
/**
|
||||
* 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<string | null> {
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user