Files
screenclipper/apps/frontend/app/settings/page.tsx
T
ayrisdevandClaude Sonnet 5 ccd922ba37 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>
2026-09-04 01:20:36 +03:00

309 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@streamclipper/db";
import {
updateYtdlpCookies,
updateSystemSettings,
updateNotificationPrefs,
updateSttEnabled,
updateAutoRenderEnabled,
updateDeleteAfterTelegramSend,
addCookieProfile,
updateCookieProfile,
deleteCookieProfile,
} from "../actions";
import { formatTr } from "../../lib/formatDate";
import { Button } from "@/components/ui/button";
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,
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;
const pollIntervalSec = pollSetting ? Math.round(Number(pollSetting.value) / 1000) : 60;
const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24;
const notifyStreamStart = notifyStart?.value !== "false";
const notifyRenderDone = notifyRender?.value !== "false";
const notifyPollError = notifyError?.value !== "false";
const sttEnabled = sttSetting?.value !== "false";
const autoRenderEnabled = autoRenderSetting?.value !== "false";
const deleteAfterTelegramSend = deleteAfterSendSetting?.value === "true";
return (
<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 (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 && 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>
{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={8}
className="font-mono-num text-xs"
/>
<Button type="submit" variant="outline" className="self-start">
Kaydet
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Sistem Parametreleri</CardTitle>
<CardDescription>Bir sonraki döngüden itibaren geçerli olur, redeploy gerekmez.</CardDescription>
</CardHeader>
<CardContent>
<form action={updateSystemSettings} className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label htmlFor="pollIntervalSec">Poll aralığı (saniye)</Label>
<Input id="pollIntervalSec" name="pollIntervalSec" type="number" min={5} defaultValue={pollIntervalSec} className="max-w-40" />
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="ttlHours">Ham segment TTL (saat)</Label>
<Input id="ttlHours" name="ttlHours" type="number" min={1} defaultValue={ttlHours} className="max-w-40" />
</div>
<p className="max-w-prose text-xs text-muted-foreground">
Eşzamanlı capture limiti ve render eşzamanlılığı BullMQ worker&apos;ları başlatılırken sabitleniyor
bunları değiştirmek için Coolify&apos;deki <code className="font-mono-num">MAX_CONCURRENT_CAPTURES</code>{" "}
/ <code className="font-mono-num">VIDEO_RENDER_CONCURRENCY</code> env var&apos;larını güncelleyip
redeploy etmek gerekiyor.
</p>
<Button type="submit" className="self-start">
Kaydet
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">STT (Transkripsiyon)</CardTitle>
<CardDescription>
Kapatırsan yeni aday klipler transkribe edilmez (PENDING_STT&apos;de bekler) OpenAI Whisper çağrısı
yapılmaz, render de tetiklenmez. Boş/konuşmasız yayınlarda (ör. sadece ambiyans müzik) gereksiz API
maliyetini önlemek için kapatabilirsin.
</CardDescription>
</CardHeader>
<form action={updateSttEnabled}>
<CardContent>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Otomatik transkripsiyon aktif
<SettingSwitch name="sttEnabled" defaultChecked={sttEnabled} />
</Label>
</CardContent>
<CardFooter>
<Button type="submit">Kaydet</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">9:16 Render</CardTitle>
<CardDescription>
Kapatırsan transkribe edilen adaylar için otomatik render tetiklenmez, klip
&quot;TRANSCRIBED&quot; durumunda bekler kanal/segment sayfalarındaki &quot;9:16 Render Et&quot;
butonuyla istediğini elle render edebilirsin.
</CardDescription>
</CardHeader>
<form action={updateAutoRenderEnabled}>
<CardContent>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Otomatik 9:16 render aktif
<SettingSwitch name="autoRenderEnabled" defaultChecked={autoRenderEnabled} />
</Label>
</CardContent>
<CardFooter>
<Button type="submit">Kaydet</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Depolama</CardTitle>
<CardDescription>
Açarsan her render biten video Telegram&apos;a gönderilip <strong>sunucudan kalıcı olarak
silinir</strong> sadece Telegram&apos;daki kopyada kalır (geri alınamaz). Gönderim başarısız olursa
dosya sunucuda kalır. Kanal sayısı arttıkça disk doluluğunu önlemek için.
</CardDescription>
</CardHeader>
<form action={updateDeleteAfterTelegramSend}>
<CardContent>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Telegram&apos;a gönderip sunucudan sil
<SettingSwitch name="deleteAfterTelegramSend" defaultChecked={deleteAfterTelegramSend} />
</Label>
</CardContent>
<CardFooter>
<Button type="submit">Kaydet</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Bildirimler</CardTitle>
</CardHeader>
<form action={updateNotificationPrefs}>
<CardContent className="flex flex-col gap-4">
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Canlı-tespit hata verince/düzelince Telegram bildirimi
<SettingSwitch name="notifyPollError" defaultChecked={notifyPollError} />
</Label>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Yayın başladığında Telegram bildirimi
<SettingSwitch name="notifyStreamStart" defaultChecked={notifyStreamStart} />
</Label>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
9:16 render tamamlandığında Telegram bildirimi
<SettingSwitch name="notifyRenderDone" defaultChecked={notifyRenderDone} />
</Label>
</CardContent>
<CardFooter>
<Button type="submit">Kaydet</Button>
</CardFooter>
</form>
</Card>
</div>
);
}