feat: 9:16 render'ı isteğe bağlı yap + Telegram'a video gönderip sunucudan sil (Faz 5)
- auto_render_enabled (varsayılan açık): kapatılınca transkript sonrası otomatik render tetiklenmez, aday TRANSCRIBED'de bekler — panelin zaten var olan "9:16 Render Et" butonuyla elle tetiklenebilir. - delete_after_telegram_send (varsayılan KAPALI — geri alınamaz bir davranış, bilinçli açılmalı): açıksa render biten video Telegram'a gerçek dosya olarak gönderilir (yeni send_telegram_video, sendVideo multipart upload, ~50MB bot-upload limiti önceden kontrol ediliyor); gönderim başarılıysa dosya diskten silinip ShortVideo.file_path NULL'a çekiliyor (DB kaydı/transkript kalıcı kalıyor), başarısızsa dosya sunucuda kalıp düz metin bildirimi gidiyor. Kanal sayısı arttıkça VPS disk doluluğunu önlemek için. - Panel: READY + file_path=null durumunda video/indir yerine "Telegram'a gönderildi, sunucuda saklanmıyor" notu (channels/[id], segments). Şema değişikliği yok — ShortVideo.filePath zaten nullable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,26 @@ export async function updateSttEnabled(formData: FormData) {
|
||||
revalidatePath("/settings");
|
||||
}
|
||||
|
||||
export async function updateAutoRenderEnabled(formData: FormData) {
|
||||
const enabled = formData.get("autoRenderEnabled") === "true";
|
||||
await prisma.appSetting.upsert({
|
||||
where: { key: "auto_render_enabled" },
|
||||
create: { key: "auto_render_enabled", value: String(enabled) },
|
||||
update: { value: String(enabled) },
|
||||
});
|
||||
revalidatePath("/settings");
|
||||
}
|
||||
|
||||
export async function updateDeleteAfterTelegramSend(formData: FormData) {
|
||||
const enabled = formData.get("deleteAfterTelegramSend") === "true";
|
||||
await prisma.appSetting.upsert({
|
||||
where: { key: "delete_after_telegram_send" },
|
||||
create: { key: "delete_after_telegram_send", value: String(enabled) },
|
||||
update: { value: String(enabled) },
|
||||
});
|
||||
revalidatePath("/settings");
|
||||
}
|
||||
|
||||
export async function updateNotificationPrefs(formData: FormData) {
|
||||
const streamStart = formData.get("notifyStreamStart") === "true";
|
||||
const renderDone = formData.get("notifyRenderDone") === "true";
|
||||
|
||||
@@ -267,7 +267,7 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
{c.short && (
|
||||
<div className="mt-2 flex flex-col items-start gap-2">
|
||||
<Badge variant={STATUS_BADGE[c.short.status] ?? "muted"}>9:16: {c.short.status}</Badge>
|
||||
{c.short.status === "READY" && (
|
||||
{c.short.status === "READY" && c.short.filePath && (
|
||||
<>
|
||||
<video controls preload="metadata" className="w-[220px] rounded-md border border-border">
|
||||
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
|
||||
@@ -277,6 +277,11 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{c.short.status === "READY" && !c.short.filePath && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
✅ Telegram'a gönderildi, sunucuda saklanmıyor.
|
||||
</p>
|
||||
)}
|
||||
{c.short.status === "FAILED" && (
|
||||
<>
|
||||
{c.short.errorMessage && (
|
||||
|
||||
@@ -89,7 +89,7 @@ export default async function SegmentsPage() {
|
||||
{c.short && (
|
||||
<div className="mt-2 flex flex-col items-start gap-2">
|
||||
<Badge variant={STATUS_BADGE[c.short.status] ?? "muted"}>9:16: {c.short.status}</Badge>
|
||||
{c.short.status === "READY" && (
|
||||
{c.short.status === "READY" && c.short.filePath && (
|
||||
<>
|
||||
<video controls preload="metadata" className="w-[220px] rounded-md border border-border">
|
||||
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
|
||||
@@ -99,6 +99,11 @@ export default async function SegmentsPage() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{c.short.status === "READY" && !c.short.filePath && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
✅ Telegram'a gönderildi, sunucuda saklanmıyor.
|
||||
</p>
|
||||
)}
|
||||
{c.short.status === "FAILED" && (
|
||||
<>
|
||||
{c.short.errorMessage && (
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { prisma } from "@streamclipper/db";
|
||||
import { updateYtdlpCookies, updateSystemSettings, updateNotificationPrefs, updateSttEnabled } from "../actions";
|
||||
import {
|
||||
updateYtdlpCookies,
|
||||
updateSystemSettings,
|
||||
updateNotificationPrefs,
|
||||
updateSttEnabled,
|
||||
updateAutoRenderEnabled,
|
||||
updateDeleteAfterTelegramSend,
|
||||
} from "../actions";
|
||||
import { formatTr } from "../../lib/formatDate";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,15 +21,18 @@ export const dynamic = "force-dynamic";
|
||||
const STALE_COOKIE_HOURS = 4;
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const [setting, pollSetting, ttlSetting, notifyStart, notifyRender, notifyError, sttSetting] = 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" } }),
|
||||
]);
|
||||
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 cookieAgeHours = setting ? (Date.now() - setting.updatedAt.getTime()) / 3_600_000 : null;
|
||||
const cookieStale = cookieAgeHours !== null && cookieAgeHours > STALE_COOKIE_HOURS;
|
||||
@@ -32,6 +42,8 @@ export default async function SettingsPage() {
|
||||
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">
|
||||
@@ -122,6 +134,50 @@ export default async function SettingsPage() {
|
||||
</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">Depolama</CardTitle>
|
||||
<CardDescription>
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user