diff --git a/.env.example b/.env.example index c8aad70..d8bca42 100644 --- a/.env.example +++ b/.env.example @@ -20,12 +20,11 @@ API_DAEMON_PORT=4001 # for a real scheduled stream. FORCE_LIVE_URL= -# base64-encoded Netscape-format cookies.txt from a logged-in YouTube session. -# Datacenter server IPs increasingly get YouTube's "Sign in to confirm -# you're not a bot" bot-check on yt-dlp requests; authenticated cookies -# avoid it. Use a throwaway/secondary Google account, not your main one — -# this file is a live session credential. Never commit the raw cookies.txt. -YTDLP_COOKIES_B64= +# YouTube cookies (Netscape cookies.txt content) are configured from the +# panel's /settings page, not here — they rotate every few hours and are +# stored in the app_settings table so they can be updated without a +# redeploy. Use a throwaway/secondary Google account, not your main one — +# it's a live session credential. # 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 diff --git a/apps/api-daemon/src/capture/streamIngest.ts b/apps/api-daemon/src/capture/streamIngest.ts index 3fcb75e..55bc1de 100644 --- a/apps/api-daemon/src/capture/streamIngest.ts +++ b/apps/api-daemon/src/capture/streamIngest.ts @@ -90,7 +90,7 @@ async function runCapture(job: Job): Promise { "yt-dlp", [ "--js-runtimes", "node", - ...ytdlpAntiBotArgs(), + ...(await ytdlpAntiBotArgs()), "-f", "bestvideo+bestaudio/best", "-o", "-", youtubeUrl, ], { stdio: ["ignore", "pipe", "pipe"] }, diff --git a/apps/api-daemon/src/env.ts b/apps/api-daemon/src/env.ts index e0c9cae..789dfa8 100644 --- a/apps/api-daemon/src/env.ts +++ b/apps/api-daemon/src/env.ts @@ -7,7 +7,6 @@ export const env = { segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 900), port: Number(process.env.API_DAEMON_PORT ?? 4001), forceLiveUrl: process.env.FORCE_LIVE_URL ?? "", - ytdlpCookiesB64: process.env.YTDLP_COOKIES_B64 ?? "", ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "", maxConcurrentCaptures: Number(process.env.MAX_CONCURRENT_CAPTURES ?? 10), }; diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts index 86e1d73..ea4b914 100644 --- a/apps/api-daemon/src/youtubePolling.ts +++ b/apps/api-daemon/src/youtubePolling.ts @@ -31,7 +31,7 @@ async function findLiveVideoId(channelId: string): Promise { try { const { stdout } = await execFileAsync( "yt-dlp", - ["--simulate", "--no-warnings", ...ytdlpAntiBotArgs(), "--print", "%(id)s", liveUrl], + ["--simulate", "--no-warnings", ...(await ytdlpAntiBotArgs()), "--print", "%(id)s", liveUrl], { timeout: 20_000 }, ); lastPollErrors.delete(channelId); diff --git a/apps/api-daemon/src/ytdlpCookies.ts b/apps/api-daemon/src/ytdlpCookies.ts index e6b203b..3717558 100644 --- a/apps/api-daemon/src/ytdlpCookies.ts +++ b/apps/api-daemon/src/ytdlpCookies.ts @@ -1,7 +1,9 @@ import { writeFileSync } from "node:fs"; +import { prisma } from "@streamclipper/db"; import { env } from "./env"; 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 @@ -9,21 +11,20 @@ const COOKIES_PATH = "/tmp/yt-cookies.txt"; * with an authenticated session's cookies) and a "The page needs to be * reloaded" proof-of-origin token check (avoided by querying the * 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 - ? (() => { - writeFileSync(COOKIES_PATH, Buffer.from(env.ytdlpCookiesB64, "base64"), { mode: 0o600 }); - return COOKIES_PATH; - })() - : null; - -export function ytdlpAntiBotArgs(): string[] { +export async function ytdlpAntiBotArgs(): Promise { const args: string[] = []; - if (cookiesFilePath) { - args.push("--cookies", cookiesFilePath); + const setting = await prisma.appSetting.findUnique({ where: { key: COOKIES_SETTING_KEY } }); + if (setting?.value) { + writeFileSync(COOKIES_PATH, setting.value, { mode: 0o600 }); + args.push("--cookies", COOKIES_PATH); } if (env.ytdlpPotProviderUrl) { diff --git a/apps/frontend/app/actions.ts b/apps/frontend/app/actions.ts index 5929768..be73c4c 100644 --- a/apps/frontend/app/actions.ts +++ b/apps/frontend/app/actions.ts @@ -38,3 +38,19 @@ export async function deleteCandidateSegment(candidateId: string) { revalidatePath("/segments"); 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"); +} diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx index c2bf62e..26d80ef 100644 --- a/apps/frontend/app/layout.tsx +++ b/apps/frontend/app/layout.tsx @@ -13,6 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children}
diff --git a/apps/frontend/app/settings/page.tsx b/apps/frontend/app/settings/page.tsx new file mode 100644 index 0000000..df9d776 --- /dev/null +++ b/apps/frontend/app/settings/page.tsx @@ -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 ( + <> +

Ayarlar

+ +
+
+ YouTube Cookie'leri + + {setting ? `son güncelleme: ${setting.updatedAt.toLocaleString("tr-TR")}` : "hiç ayarlanmadı"} + +
+

+ 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. +

+
+