feat: YouTube cookie'lerini panelden yönetilebilir hale getir
Cookie'ler saatler içinde eskiyordu, her seferinde manuel Coolify env güncellemesi + redeploy gerekiyordu. Artık DB'de (app_settings tablosu) tutuluyor; /settings sayfasından yeni cookies.txt yapıştırılıp kaydedilebiliyor, bir sonraki yt-dlp çağrısında redeploy gerekmeden devreye giriyor. YTDLP_COOKIES_B64 env var mekanizması kaldırıldı. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+5
-6
@@ -20,12 +20,11 @@ API_DAEMON_PORT=4001
|
|||||||
# for a real scheduled stream.
|
# for a real scheduled stream.
|
||||||
FORCE_LIVE_URL=
|
FORCE_LIVE_URL=
|
||||||
|
|
||||||
# base64-encoded Netscape-format cookies.txt from a logged-in YouTube session.
|
# YouTube cookies (Netscape cookies.txt content) are configured from the
|
||||||
# Datacenter server IPs increasingly get YouTube's "Sign in to confirm
|
# panel's /settings page, not here — they rotate every few hours and are
|
||||||
# you're not a bot" bot-check on yt-dlp requests; authenticated cookies
|
# stored in the app_settings table so they can be updated without a
|
||||||
# avoid it. Use a throwaway/secondary Google account, not your main one —
|
# redeploy. Use a throwaway/secondary Google account, not your main one —
|
||||||
# this file is a live session credential. Never commit the raw cookies.txt.
|
# it's a live session credential.
|
||||||
YTDLP_COOKIES_B64=
|
|
||||||
|
|
||||||
# URL of a bgutil-ytdlp-pot-provider HTTP server (see docker-compose.yml
|
# URL of a bgutil-ytdlp-pot-provider HTTP server (see docker-compose.yml
|
||||||
# sc_pot_provider). Needed alongside cookies to avoid YouTube's "The page
|
# sc_pot_provider). Needed alongside cookies to avoid YouTube's "The page
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
|||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
[
|
[
|
||||||
"--js-runtimes", "node",
|
"--js-runtimes", "node",
|
||||||
...ytdlpAntiBotArgs(),
|
...(await ytdlpAntiBotArgs()),
|
||||||
"-f", "bestvideo+bestaudio/best", "-o", "-", youtubeUrl,
|
"-f", "bestvideo+bestaudio/best", "-o", "-", youtubeUrl,
|
||||||
],
|
],
|
||||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const env = {
|
|||||||
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 900),
|
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 900),
|
||||||
port: Number(process.env.API_DAEMON_PORT ?? 4001),
|
port: Number(process.env.API_DAEMON_PORT ?? 4001),
|
||||||
forceLiveUrl: process.env.FORCE_LIVE_URL ?? "",
|
forceLiveUrl: process.env.FORCE_LIVE_URL ?? "",
|
||||||
ytdlpCookiesB64: process.env.YTDLP_COOKIES_B64 ?? "",
|
|
||||||
ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "",
|
ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "",
|
||||||
maxConcurrentCaptures: Number(process.env.MAX_CONCURRENT_CAPTURES ?? 10),
|
maxConcurrentCaptures: Number(process.env.MAX_CONCURRENT_CAPTURES ?? 10),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function findLiveVideoId(channelId: string): Promise<string | null> {
|
|||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync(
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
["--simulate", "--no-warnings", ...ytdlpAntiBotArgs(), "--print", "%(id)s", liveUrl],
|
["--simulate", "--no-warnings", ...(await ytdlpAntiBotArgs()), "--print", "%(id)s", liveUrl],
|
||||||
{ timeout: 20_000 },
|
{ timeout: 20_000 },
|
||||||
);
|
);
|
||||||
lastPollErrors.delete(channelId);
|
lastPollErrors.delete(channelId);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
|
import { prisma } from "@streamclipper/db";
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
|
|
||||||
const COOKIES_PATH = "/tmp/yt-cookies.txt";
|
const COOKIES_PATH = "/tmp/yt-cookies.txt";
|
||||||
|
const COOKIES_SETTING_KEY = "ytdlp_cookies";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YouTube's anti-bot layer against datacenter IPs has two parts, both
|
* YouTube's anti-bot layer against datacenter IPs has two parts, both
|
||||||
@@ -9,21 +11,20 @@ const COOKIES_PATH = "/tmp/yt-cookies.txt";
|
|||||||
* with an authenticated session's cookies) and a "The page needs to be
|
* with an authenticated session's cookies) and a "The page needs to be
|
||||||
* reloaded" proof-of-origin token check (avoided by querying the
|
* reloaded" proof-of-origin token check (avoided by querying the
|
||||||
* bgutil-ytdlp-pot-provider sidecar — see docker-compose.yml sc_pot_provider).
|
* bgutil-ytdlp-pot-provider sidecar — see docker-compose.yml sc_pot_provider).
|
||||||
* The cookies file is never committed — it's decoded once at startup from a
|
*
|
||||||
* Coolify-managed env var into a local temp file.
|
* Cookies rotate/expire over hours, so they're stored in the DB (updated
|
||||||
|
* from the panel's Settings page — see apps/frontend/app/settings) rather
|
||||||
|
* than baked in at container start from an env var. Every call re-reads the
|
||||||
|
* current value and rewrites the temp file, so a panel update takes effect
|
||||||
|
* on the very next yt-dlp invocation without a redeploy.
|
||||||
*/
|
*/
|
||||||
const cookiesFilePath: string | null = env.ytdlpCookiesB64
|
export async function ytdlpAntiBotArgs(): Promise<string[]> {
|
||||||
? (() => {
|
|
||||||
writeFileSync(COOKIES_PATH, Buffer.from(env.ytdlpCookiesB64, "base64"), { mode: 0o600 });
|
|
||||||
return COOKIES_PATH;
|
|
||||||
})()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
export function ytdlpAntiBotArgs(): string[] {
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (cookiesFilePath) {
|
const setting = await prisma.appSetting.findUnique({ where: { key: COOKIES_SETTING_KEY } });
|
||||||
args.push("--cookies", cookiesFilePath);
|
if (setting?.value) {
|
||||||
|
writeFileSync(COOKIES_PATH, setting.value, { mode: 0o600 });
|
||||||
|
args.push("--cookies", COOKIES_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.ytdlpPotProviderUrl) {
|
if (env.ytdlpPotProviderUrl) {
|
||||||
|
|||||||
@@ -38,3 +38,19 @@ export async function deleteCandidateSegment(candidateId: string) {
|
|||||||
revalidatePath("/segments");
|
revalidatePath("/segments");
|
||||||
revalidatePath("/", "layout");
|
revalidatePath("/", "layout");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateYtdlpCookies(formData: FormData) {
|
||||||
|
const value = String(formData.get("cookies") ?? "").trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("Cookie içeriği boş olamaz.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.appSetting.upsert({
|
||||||
|
where: { key: "ytdlp_cookies" },
|
||||||
|
create: { key: "ytdlp_cookies", value },
|
||||||
|
update: { value },
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath("/settings");
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<nav className="nav">
|
<nav className="nav">
|
||||||
<Link href="/">Kanal Durumu</Link>
|
<Link href="/">Kanal Durumu</Link>
|
||||||
<Link href="/segments">Segment & Aday Kütüphanesi</Link>
|
<Link href="/segments">Segment & Aday Kütüphanesi</Link>
|
||||||
|
<Link href="/settings">Ayarlar</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { prisma } from "@streamclipper/db";
|
||||||
|
import { updateYtdlpCookies } from "../actions";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function SettingsPage() {
|
||||||
|
const setting = await prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Ayarlar</h1>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-head">
|
||||||
|
<span>YouTube Cookie'leri</span>
|
||||||
|
<span className="mono">
|
||||||
|
{setting ? `son güncelleme: ${setting.updatedAt.toLocaleString("tr-TR")}` : "hiç ayarlanmadı"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="empty" style={{ marginBottom: "0.5rem" }}>
|
||||||
|
YouTube canlı yayın kontrolü ve kayıt için kullanılıyor. Google, oturum çerezlerinin bir
|
||||||
|
kısmını birkaç saatte bir yeniliyor — burada eskidiğini fark edersen (kanal detay
|
||||||
|
sayfasında "son poll hatası" olarak görürsün) taze bir cookies.txt ile
|
||||||
|
güncelle. Ana hesabın yerine ayrı, önemsiz bir Google hesabı kullanman önerilir.
|
||||||
|
</p>
|
||||||
|
<form action={updateYtdlpCookies}>
|
||||||
|
<textarea
|
||||||
|
name="cookies"
|
||||||
|
required
|
||||||
|
placeholder="Netscape formatlı cookies.txt içeriğini buraya yapıştır"
|
||||||
|
rows={10}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
background: "var(--bg)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "0.6rem 0.7rem",
|
||||||
|
color: "var(--text)",
|
||||||
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
resize: "vertical",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{
|
||||||
|
marginTop: "0.6rem",
|
||||||
|
background: "var(--accent)",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kaydet
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,7 +38,6 @@ services:
|
|||||||
FORCE_LIVE_URL: ${FORCE_LIVE_URL:-}
|
FORCE_LIVE_URL: ${FORCE_LIVE_URL:-}
|
||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
YTDLP_COOKIES_B64: ${YTDLP_COOKIES_B64:-}
|
|
||||||
YTDLP_POT_PROVIDER_URL: http://sc_pot_provider:4416
|
YTDLP_POT_PROVIDER_URL: http://sc_pot_provider:4416
|
||||||
MAX_CONCURRENT_CAPTURES: ${MAX_CONCURRENT_CAPTURES:-10}
|
MAX_CONCURRENT_CAPTURES: ${MAX_CONCURRENT_CAPTURES:-10}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "app_settings" (
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "app_settings_pkey" PRIMARY KEY ("key")
|
||||||
|
);
|
||||||
@@ -76,3 +76,11 @@ model CandidateSegment {
|
|||||||
|
|
||||||
@@map("candidate_segments")
|
@@map("candidate_segments")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model AppSetting {
|
||||||
|
key String @id
|
||||||
|
value String
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("app_settings")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user