feat: birden fazla YouTube hesabı arasında cookie'yi dağıt (Faz 6)

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-<id>.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 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 01:20:36 +03:00
co-authored by Claude Sonnet 5
parent eb2a9d28ed
commit ccd922ba37
5 changed files with 215 additions and 26 deletions
+121 -20
View File
@@ -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() {
<div className="flex flex-col gap-6">
<h1 className="text-2xl font-semibold tracking-tight">Ayarlar</h1>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Cookie Hesapları</CardTitle>
<CardDescription>
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
&quot;Sign in to confirm you&apos;re not a bot&quot; 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ı.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{cookieProfiles.length === 0 ? (
<p className="text-sm text-muted-foreground">
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.
</p>
) : (
<div className="flex flex-col gap-3">
{cookieProfiles.map((profile) => {
const ageHours = (Date.now() - profile.updatedAt.getTime()) / 3_600_000;
const stale = ageHours > STALE_COOKIE_HOURS;
return (
<div key={profile.id} className="rounded-lg border border-border p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">{profile.label}</span>
<div className="flex items-center gap-2">
<span className="font-mono-num text-xs text-muted-foreground">
{formatTr(profile.updatedAt)}
</span>
<form action={deleteCookieProfile.bind(null, profile.id)}>
<ConfirmButton confirmText={`${profile.label} hesabını silmek istediğine emin misin?`} label="Sil" />
</form>
</div>
</div>
{stale && (
<Badge variant="warn" className="mt-2 w-fit">
{Math.round(ageHours)} saattir güncellenmedi
</Badge>
)}
<details className="mt-2 text-sm">
<summary className="cursor-pointer font-mono-num text-xs text-muted-foreground">
Cookie&apos;yi yenile
</summary>
<form action={updateCookieProfile.bind(null, profile.id)} className="mt-2 flex flex-col gap-2">
<Textarea
name="value"
required
placeholder="Netscape formatlı cookies.txt içeriğini buraya yapıştır"
rows={6}
className="font-mono-num text-xs"
/>
<Button type="submit" size="sm" className="self-start">
Kaydet
</Button>
</form>
</details>
</div>
);
})}
</div>
)}
<Separator />
<form action={addCookieProfile} className="flex flex-col gap-2">
<Label htmlFor="newProfileLabel" className="text-xs text-muted-foreground">
Yeni hesap ekle
</Label>
<Input id="newProfileLabel" name="label" placeholder="Hesap adı (ör. Hesap 2)" required />
<Textarea
name="value"
required
placeholder="Netscape formatlı cookies.txt içeriğini buraya yapıştır"
rows={6}
className="font-mono-num text-xs"
/>
<Button type="submit" className="self-start">
Hesap Ekle
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader className="flex-row items-center justify-between">
<CardTitle className="text-sm font-medium text-muted-foreground">YouTube Cookie&apos;leri</CardTitle>
<CardTitle className="text-sm font-medium text-muted-foreground">
YouTube Cookie&apos;leri (tekli / eski)
</CardTitle>
<span className="font-mono-num text-xs text-muted-foreground">
{setting ? `son güncelleme: ${formatTr(setting.updatedAt)}` : "hiç ayarlanmadı"}
</span>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{cookieStale && (
{cookieStale && cookieProfiles.length === 0 && (
<Badge variant="warn" className="w-fit">
{Math.round(cookieAgeHours!)} saattir güncellenmedi büyük/popüler kanallarda bot-check hatası
görülebilir
</Badge>
)}
<CardDescription>
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 &quot;son poll hatası&quot;
olarak görürsün) taze bir cookies.txt ile güncelle. Ana hesabın yerine ayrı, önemsiz bir Google hesabı
kullanman önerilir.
{cookieProfiles.length > 0
? "Yukarıda en az bir hesap eklendiği için bu ayar artık kullanılmıyor — sadece geriye dönük referans için duruyor."
: "Yukarıda hiç hesap eklenmediği sürece tüm kanallar bu tek cookie'yi paylaşır."}
</CardDescription>
<form action={updateYtdlpCookies} className="flex flex-col gap-3">
<Textarea
name="cookies"
required
placeholder="Netscape formatlı cookies.txt içeriğini buraya yapıştır"
rows={10}
rows={8}
className="font-mono-num text-xs"
/>
<Button type="submit" className="self-start">
<Button type="submit" variant="outline" className="self-start">
Kaydet
</Button>
</form>