fix: canlı-tespitini yt-dlp'den düz HTTP fetch'e taşı
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/<id>/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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,47 +1,45 @@
|
|||||||
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* yt-dlp exits non-zero both when a channel isn't live and when the check
|
* Live detection failures are kept per channel and surfaced via GET /status
|
||||||
* itself fails (network block, bot detection, extractor breakage). Those two
|
* for debugging.
|
||||||
* cases must not be indistinguishable, so the last failure per channel is
|
|
||||||
* kept here and surfaced via GET /status for debugging.
|
|
||||||
*/
|
*/
|
||||||
export const lastPollErrors = new Map<string, string>();
|
export const lastPollErrors = new Map<string, string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live detection goes through yt-dlp itself instead of the YouTube Data API.
|
* Live detection is a plain page fetch, not a yt-dlp invocation. Resolving
|
||||||
* `search.list?eventType=live` costs 100 quota units per call against a
|
* downloadable formats (what yt-dlp does even in --simulate mode) is what
|
||||||
* 10,000/day free quota — polling one channel every 60s alone would need
|
* triggers YouTube's bot-check and proof-of-origin token requirements — but
|
||||||
* ~144,000 units/day, well over quota. yt-dlp's `/live` redirect check is
|
* we don't need stream bytes here, just whether the channel is live, which
|
||||||
* free and needs no API key.
|
* 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<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) {
|
||||||
[
|
lastPollErrors.set(channelId, `HTTP ${res.status} fetching ${liveUrl}`);
|
||||||
"--simulate",
|
return null;
|
||||||
"--no-warnings",
|
}
|
||||||
...ytdlpAntiBotArgs(),
|
|
||||||
"-f", "bestvideo+bestaudio/best",
|
const html = await res.text();
|
||||||
"--print", "%(id)s",
|
|
||||||
liveUrl,
|
|
||||||
],
|
|
||||||
{ timeout: 20_000 },
|
|
||||||
);
|
|
||||||
lastPollErrors.delete(channelId);
|
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) {
|
} 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