Files
screenclipper/apps/frontend/app/settings/page.tsx
T
ayrisdevandClaude Sonnet 5 11e98b300e feat: analiz zincirini atlayıp ham segmenti doğrudan Telegram'a gönderme modu
Yeni "Ham Kayıt Modu" ayarı (send_raw_segments_to_telegram, varsayılan
kapalı): açıksa her ham segment tamamlanır tamamlanmaz sinyal analizi/
STT/9:16 render zincirine hiç girmeden doğrudan Telegram'a gönderiliyor
ve sunucudan siliniyor. Kullanıcı transkript/9:16 istemediğinde ("sen
bana direk videoyu gönder") tüm analiz pipeline'ını atlayıp sadece
kayıt+gönder+sil yapan basit bir mod.

- RawSegmentStatus'a SENT_TO_TELEGRAM eklendi, RawSegment.filePath
  nullable yapıldı (aynı ShortVideo.filePath deseni — gönderim sonrası
  null, dosya diskten silinmiş demek).
- api-daemon/src/telegram.ts'e sendTelegramVideo eklendi (native fetch+
  FormData+Blob ile multipart upload, ~50MB bot-limiti önceden kontrol
  ediliyor) — worker/telegram.py'deki aynı fonksiyonun Node karşılığı.
- streamIngest.ts: segment tamamlanınca, mod açıksa signal-detection
  kuyruğuna hiç eklemeden doğrudan gönderiyor (kuyruğa eklemek, aşağıda
  dosyayı silmenin sinyal analizini bozmasına sebep olurdu).
- Panel: segment listelerinde filePath=null durumunda video/indir yerine
  "Telegram'a gönderildi" notu (segments, channels/[id]).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 19:57:14 +03:00

338 lines
16 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,
updateSendRawSegmentsToTelegram,
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,
sendRawSetting,
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.appSetting.findUnique({ where: { key: "send_raw_segments_to_telegram" } }),
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";
const sendRawSegmentsToTelegram = sendRawSetting?.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">Ham Kayıt Modu</CardTitle>
<CardDescription>
Açarsan her ham segment (analiz/transkript/9:16 zincirine hiç girmeden) tamamlanır tamamlanmaz doğrudan
Telegram&apos;a gönderilip sunucudan silinir transkript ve 9:16 render tamamen atlanır. Bunu açtıysan
STT ve 9:16 Render ayarlarının kapalı olması mantıklı (aksi halde ikisi de boşa kalır, çünkü bu mod
segmenti analiz kuyruğuna hiç sokmuyor). ~50MB&apos;ı geçen segmentler gönderilemez, sunucuda kalır
gerekirse yukarıdaki &quot;Sistem Parametreleri&quot;nden segment süresini kısalt.
</CardDescription>
</CardHeader>
<form action={updateSendRawSegmentsToTelegram}>
<CardContent>
<Label className="flex items-center justify-between gap-4 text-sm font-normal">
Ham segmenti doğrudan Telegram&apos;a gönder (analiz atlanır)
<SettingSwitch name="sendRawSegmentsToTelegram" defaultChecked={sendRawSegmentsToTelegram} />
</Label>
</CardContent>
<CardFooter>
<Button type="submit">Kaydet</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Depolama (9:16 klipler)</CardTitle>
<CardDescription>
Yukarıdaki &quot;Ham Kayıt Modu&quot;ndan farklı bu, STT+9:16 render zincirinden geçmiş klipler için.
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>
);
}