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>
338 lines
16 KiB
TypeScript
338 lines
16 KiB
TypeScript
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
|
||
"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ı.
|
||
</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'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'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'ları başlatılırken sabitleniyor —
|
||
bunları değiştirmek için Coolify'deki <code className="font-mono-num">MAX_CONCURRENT_CAPTURES</code>{" "}
|
||
/ <code className="font-mono-num">VIDEO_RENDER_CONCURRENCY</code> env var'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'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
|
||
"TRANSCRIBED" durumunda bekler — kanal/segment sayfalarındaki "9:16 Render Et"
|
||
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'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'ı geçen segmentler gönderilemez, sunucuda kalır —
|
||
gerekirse yukarıdaki "Sistem Parametreleri"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'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 "Ham Kayıt Modu"ndan farklı — bu, STT+9:16 render zincirinden geçmiş klipler için.
|
||
Açarsan her render biten video Telegram'a gönderilip <strong>sunucudan kalıcı olarak
|
||
silinir</strong> — sadece Telegram'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'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>
|
||
);
|
||
}
|