Files
screenclipper/apps/frontend/app/actions.ts
T
ayrisdevandClaude Sonnet 5 eb2a9d28ed 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>
2026-09-04 01:04:57 +03:00

217 lines
7.9 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.
"use server";
import { unlink } from "node:fs/promises";
import { prisma } from "@streamclipper/db";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { postToApiDaemon } from "../lib/apiDaemon";
export async function addChannel(formData: FormData) {
const name = String(formData.get("name") ?? "").trim();
const youtubeHandle = String(formData.get("youtubeHandle") ?? "").trim();
const channelId = String(formData.get("channelId") ?? "").trim();
if (!name || !youtubeHandle || !channelId) {
throw new Error("Kanal adı, handle ve channel_id alanları zorunlu.");
}
await prisma.channel.create({
data: { name, youtubeHandle, channelId, isActive: true },
});
revalidatePath("/");
}
/** ShortVideo has a required FK to CandidateSegment (candidate_id) — every deletion path that removes candidates must clear their shorts (row + rendered file) first, or Postgres rejects the candidate delete. */
async function deleteShortsForCandidates(candidateIds: string[]): Promise<void> {
if (candidateIds.length === 0) return;
const shorts = await prisma.shortVideo.findMany({ where: { candidateId: { in: candidateIds } } });
await prisma.shortVideo.deleteMany({ where: { candidateId: { in: candidateIds } } });
await Promise.all(shorts.filter((s) => s.filePath).map((s) => unlink(s.filePath!).catch(() => {})));
}
export async function deleteRawSegment(segmentId: string) {
const segment = await prisma.rawSegment.findUnique({
where: { id: segmentId },
include: { candidates: true },
});
if (!segment) return;
await deleteShortsForCandidates(segment.candidates.map((c) => c.id));
await prisma.candidateSegment.deleteMany({ where: { rawSegmentId: segmentId } });
await prisma.rawSegment.delete({ where: { id: segmentId } });
await unlink(segment.filePath).catch(() => {});
revalidatePath("/segments");
revalidatePath("/", "layout");
}
export async function deleteCandidateSegment(candidateId: string) {
await deleteShortsForCandidates([candidateId]);
await prisma.candidateSegment.delete({ where: { id: candidateId } });
revalidatePath("/segments");
revalidatePath("/", "layout");
}
export async function deleteChannel(channelId: string) {
const sessions = await prisma.streamSession.findMany({
where: { channelId },
include: { segments: { include: { candidates: true } } },
});
const rawSegmentIds = sessions.flatMap((s) => s.segments.map((seg) => seg.id));
const candidateIds = sessions.flatMap((s) => s.segments.flatMap((seg) => seg.candidates.map((c) => c.id)));
const filePaths = sessions.flatMap((s) => s.segments.map((seg) => seg.filePath));
await deleteShortsForCandidates(candidateIds);
await prisma.candidateSegment.deleteMany({ where: { rawSegmentId: { in: rawSegmentIds } } });
await prisma.rawSegment.deleteMany({ where: { sessionId: { in: sessions.map((s) => s.id) } } });
await prisma.streamSession.deleteMany({ where: { channelId } });
await prisma.channel.delete({ where: { id: channelId } });
await Promise.all(filePaths.map((p) => unlink(p).catch(() => {})));
revalidatePath("/", "layout");
redirect("/");
}
export async function toggleChannelActive(channelId: string, nextActive: boolean) {
await prisma.channel.update({ where: { id: channelId }, data: { isActive: nextActive } });
revalidatePath("/", "layout");
}
export async function updateChannelSegmentTime(channelId: string, formData: FormData) {
const raw = String(formData.get("segmentTimeMin") ?? "").trim();
const minutes = raw === "" ? null : Number(raw);
const seconds = minutes !== null && Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes * 60) : null;
await prisma.channel.update({ where: { id: channelId }, data: { segmentTimeSec: seconds } });
revalidatePath(`/channels/${channelId}`);
}
export async function toggleAllChannels(nextActive: boolean) {
await prisma.channel.updateMany({ data: { isActive: nextActive } });
revalidatePath("/", "layout");
}
export async function stopSession(sessionId: string) {
await postToApiDaemon(`/sessions/${sessionId}/stop`);
revalidatePath("/", "layout");
}
export async function checkChannelNow(channelId: string) {
await postToApiDaemon(`/channels/${channelId}/check-now`);
revalidatePath("/", "layout");
}
export async function forceStartCapture(channelId: string, formData: FormData) {
const url = String(formData.get("url") ?? "").trim();
if (!url) throw new Error("YouTube URL zorunlu.");
await postToApiDaemon(`/channels/${channelId}/force-start`, { url });
revalidatePath("/", "layout");
}
export async function retryRender(candidateId: string) {
await postToApiDaemon(`/shorts/${candidateId}/render`);
revalidatePath("/segments");
revalidatePath("/", "layout");
}
export async function cancelRender(candidateId: string) {
await postToApiDaemon(`/shorts/${candidateId}/cancel`);
revalidatePath("/segments");
revalidatePath("/", "layout");
}
export async function updateSystemSettings(formData: FormData) {
const pollSeconds = Number(formData.get("pollIntervalSec"));
const ttlHours = Number(formData.get("ttlHours"));
if (Number.isFinite(pollSeconds) && pollSeconds >= 5) {
await prisma.appSetting.upsert({
where: { key: "poll_interval_ms" },
create: { key: "poll_interval_ms", value: String(pollSeconds * 1000) },
update: { value: String(pollSeconds * 1000) },
});
}
if (Number.isFinite(ttlHours) && ttlHours > 0) {
await prisma.appSetting.upsert({
where: { key: "raw_segment_ttl_hours" },
create: { key: "raw_segment_ttl_hours", value: String(ttlHours) },
update: { value: String(ttlHours) },
});
}
revalidatePath("/settings");
}
export async function updateSttEnabled(formData: FormData) {
const enabled = formData.get("sttEnabled") === "true";
await prisma.appSetting.upsert({
where: { key: "stt_enabled" },
create: { key: "stt_enabled", value: String(enabled) },
update: { value: String(enabled) },
});
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";
const pollError = formData.get("notifyPollError") === "true";
await prisma.appSetting.upsert({
where: { key: "notify_poll_error" },
create: { key: "notify_poll_error", value: String(pollError) },
update: { value: String(pollError) },
});
await prisma.appSetting.upsert({
where: { key: "notify_stream_start" },
create: { key: "notify_stream_start", value: String(streamStart) },
update: { value: String(streamStart) },
});
await prisma.appSetting.upsert({
where: { key: "notify_render_done" },
create: { key: "notify_render_done", value: String(renderDone) },
update: { value: String(renderDone) },
});
revalidatePath("/settings");
}
export async function updateYtdlpCookies(formData: FormData) {
const value = String(formData.get("cookies") ?? "").trim();
if (!value) {
throw new Error("Cookie içeriği boş olamaz.");
}
await prisma.appSetting.upsert({
where: { key: "ytdlp_cookies" },
create: { key: "ytdlp_cookies", value },
update: { value },
});
revalidatePath("/settings");
}