From ccd922ba3798c701862e09def31018000446bd7d Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Fri, 4 Sep 2026 01:20:36 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20birden=20fazla=20YouTube=20hesab=C4=B1?= =?UTF-8?q?=20aras=C4=B1nda=20cookie'yi=20da=C4=9F=C4=B1t=20(Faz=206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proxy IP'lerini havuza dağıttığımız aynı sorun cookie/hesap seviyesinde de vardı: tek bir Google hesabı 7-8 kanalı taramaya çalışınca YouTube hesabın kendisini şüpheli bulup çoğu/tüm kanalda "Sign in to confirm you're not a bot" veriyordu — proxy IP'den bağımsız bir sinyal, IP dağıtımı bunu çözemez. Yeni CookieProfile tablosu: her kanal, DB id'sinden aynı deterministik hash ile (proxyUrlForChannel'daki gibi) havuzdaki hesaplardan birine sabitlenir. Her profil kendi ayrı geçici dosyasına yazılıyor (/tmp/yt-cookies-.txt) — tek paylaşılan dosya, farklı profil kullanan kanallar eşzamanlı çalışınca birbirinin cookie'sini ezerdi. Hiç profil eklenmemişse eski tekli ytdlp_cookies ayarına geri düşüyor (geriye dönük uyumlu, hemen kırılmaz). Ayarlar sayfasında yeni "Cookie Hesapları" kartı: hesap ekle/sil/yenile, her biri için ayrı bayatlık rozeti. Eski tekli cookie kartı "tekli/eski" etiketiyle fallback olarak duruyor. Co-Authored-By: Claude Sonnet 5 --- apps/api-daemon/src/ytdlpCookies.ts | 48 +++++- apps/frontend/app/actions.ts | 27 ++++ apps/frontend/app/settings/page.tsx | 141 +++++++++++++++--- .../migration.sql | 10 ++ packages/db/prisma/schema.prisma | 15 ++ 5 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 packages/db/prisma/migrations/20260903000000_cookie_profiles/migration.sql diff --git a/apps/api-daemon/src/ytdlpCookies.ts b/apps/api-daemon/src/ytdlpCookies.ts index 3ab48a3..b768133 100644 --- a/apps/api-daemon/src/ytdlpCookies.ts +++ b/apps/api-daemon/src/ytdlpCookies.ts @@ -2,8 +2,8 @@ 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"; +const LEGACY_COOKIES_PATH = "/tmp/yt-cookies.txt"; +const LEGACY_COOKIES_SETTING_KEY = "ytdlp_cookies"; // Oxylabs' static ISP pool: one dedicated (non-rotating) IP per port, // confirmed against the dashboard's Proxy list (isp.oxylabs.io:8001..8010 -> @@ -30,6 +30,43 @@ function proxyUrlForChannel(baseProxyUrl: string, channelKey: string): string { return `${prefix}${basePort + offset}`; } +/** + * Picks which cookie profile a channel uses (same deterministic-hash + * pinning as proxyUrlForChannel — same channel always gets the same + * account, different channels spread across the pool) and writes it to a + * profile-specific temp file. Falls back to the old single global + * ytdlp_cookies AppSetting if no profiles have been added yet, so nothing + * breaks before the panel's Settings page is used to add accounts. + * + * A single shared cookie file was a real bottleneck once several channels + * were active at once: it isn't just proxy IP reputation that YouTube can + * flag, it's the *account* — one Google session making requests that look + * like it's scanning many unrelated channels trips its own abuse signal, + * independent of which IP each request came from. Splitting across + * separate throwaway accounts dilutes that per-account signal the same way + * the proxy pool dilutes per-IP signal. + * + * Profile-specific file paths (not one shared /tmp/yt-cookies.txt) matter + * once multiple channels can be using *different* profiles concurrently — + * a shared path would let one channel's capture process read a file another + * channel's poll check just overwrote with a different account's cookies. + */ +async function cookiesFilePathForChannel(channelKey: string): Promise { + const profiles = await prisma.cookieProfile.findMany({ orderBy: { createdAt: "asc" } }); + + if (profiles.length === 0) { + const legacy = await prisma.appSetting.findUnique({ where: { key: LEGACY_COOKIES_SETTING_KEY } }); + if (!legacy?.value) return null; + writeFileSync(LEGACY_COOKIES_PATH, legacy.value, { mode: 0o600 }); + return LEGACY_COOKIES_PATH; + } + + const profile = profiles[simpleHash(channelKey) % profiles.length]; + const path = `/tmp/yt-cookies-${profile.id}.txt`; + writeFileSync(path, profile.value, { mode: 0o600 }); + return path; +} + /** * YouTube's anti-bot layer against yt-dlp has three parts, all needed * together: a "Sign in to confirm you're not a bot" wall (avoided with an @@ -52,10 +89,9 @@ export async function ytdlpAntiBotArgs( ): Promise { const args: string[] = []; - 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); + const cookiesPath = await cookiesFilePathForChannel(channelKey); + if (cookiesPath) { + args.push("--cookies", cookiesPath); } if (env.ytdlpPotProviderUrl) { diff --git a/apps/frontend/app/actions.ts b/apps/frontend/app/actions.ts index 5bcc922..9f1426f 100644 --- a/apps/frontend/app/actions.ts +++ b/apps/frontend/app/actions.ts @@ -214,3 +214,30 @@ export async function updateYtdlpCookies(formData: FormData) { revalidatePath("/settings"); } + +export async function addCookieProfile(formData: FormData) { + const label = String(formData.get("label") ?? "").trim(); + const value = String(formData.get("value") ?? "").trim(); + + if (!label || !value) { + throw new Error("Hesap adı ve cookie içeriği zorunlu."); + } + + await prisma.cookieProfile.create({ data: { label, value } }); + revalidatePath("/settings"); +} + +export async function updateCookieProfile(profileId: string, formData: FormData) { + const value = String(formData.get("value") ?? "").trim(); + if (!value) { + throw new Error("Cookie içeriği boş olamaz."); + } + + await prisma.cookieProfile.update({ where: { id: profileId }, data: { value } }); + revalidatePath("/settings"); +} + +export async function deleteCookieProfile(profileId: string) { + await prisma.cookieProfile.delete({ where: { id: profileId } }); + revalidatePath("/settings"); +} diff --git a/apps/frontend/app/settings/page.tsx b/apps/frontend/app/settings/page.tsx index 482215a..8b52be9 100644 --- a/apps/frontend/app/settings/page.tsx +++ b/apps/frontend/app/settings/page.tsx @@ -6,6 +6,9 @@ import { updateSttEnabled, updateAutoRenderEnabled, updateDeleteAfterTelegramSend, + addCookieProfile, + updateCookieProfile, + deleteCookieProfile, } from "../actions"; import { formatTr } from "../../lib/formatDate"; import { Button } from "@/components/ui/button"; @@ -13,26 +16,39 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { SettingSwitch } from "../components/SettingSwitch"; +import { ConfirmButton } from "../components/ConfirmButton"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; export const dynamic = "force-dynamic"; const STALE_COOKIE_HOURS = 4; export default async function SettingsPage() { - const [setting, pollSetting, ttlSetting, notifyStart, notifyRender, notifyError, sttSetting, autoRenderSetting, deleteAfterSendSetting] = - await Promise.all([ - prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } }), - prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }), - prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }), - prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }), - prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }), - prisma.appSetting.findUnique({ where: { key: "notify_poll_error" } }), - prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }), - prisma.appSetting.findUnique({ where: { key: "auto_render_enabled" } }), - prisma.appSetting.findUnique({ where: { key: "delete_after_telegram_send" } }), - ]); + const [ + setting, + pollSetting, + ttlSetting, + notifyStart, + notifyRender, + notifyError, + sttSetting, + autoRenderSetting, + deleteAfterSendSetting, + cookieProfiles, + ] = await Promise.all([ + prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } }), + prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }), + prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }), + prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }), + prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }), + prisma.appSetting.findUnique({ where: { key: "notify_poll_error" } }), + prisma.appSetting.findUnique({ where: { key: "stt_enabled" } }), + prisma.appSetting.findUnique({ where: { key: "auto_render_enabled" } }), + prisma.appSetting.findUnique({ where: { key: "delete_after_telegram_send" } }), + prisma.cookieProfile.findMany({ orderBy: { createdAt: "asc" } }), + ]); const cookieAgeHours = setting ? (Date.now() - setting.updatedAt.getTime()) / 3_600_000 : null; const cookieStale = cookieAgeHours !== null && cookieAgeHours > STALE_COOKIE_HOURS; @@ -49,35 +65,120 @@ export default async function SettingsPage() {

Ayarlar

+ + + Cookie Hesapları + + Her kanal, aşağıdaki hesaplardan birine sabit olarak atanır (aynı kanal hep aynı hesabı kullanır, kanallar + havuza dağılır). Tek hesap çok kanalı taramaya çalışınca YouTube hesabın kendisini şüpheli bulup + "Sign in to confirm you're not a bot" hatası verebiliyor — birkaç ayrı, önemsiz Google + hesabı ekleyerek bu yükü dağıt. Her hesap ayrı bir tarayıcıda giriş yapılıp cookies.txt olarak dışa + aktarılmalı. + + + + {cookieProfiles.length === 0 ? ( +

+ Henüz hesap eklenmedi — aşağıdan en az bir tane ekle. Eklenene kadar aşağıdaki tekli/eski cookie ayarı + kullanılmaya devam eder. +

+ ) : ( +
+ {cookieProfiles.map((profile) => { + const ageHours = (Date.now() - profile.updatedAt.getTime()) / 3_600_000; + const stale = ageHours > STALE_COOKIE_HOURS; + return ( +
+
+ {profile.label} +
+ + {formatTr(profile.updatedAt)} + +
+ + +
+
+ {stale && ( + + ⚠️ {Math.round(ageHours)} saattir güncellenmedi + + )} +
+ + Cookie'yi yenile + +
+