Files
screenclipper/apps/frontend/app/actions.ts
T
ayrisdevandClaude Sonnet 5 d045b24c85 fix: silme fonksiyonlarında ShortVideo FK ihlali (kanal/segment/candidate sil hata veriyordu)
Faz 2'de ShortVideo eklenirken deleteRawSegment/deleteCandidateSegment/
deleteChannel güncellenmemiş — candidate_id FK'sı yüzünden bir short'u
olan candidate silinmeye çalışılınca Postgres reddediyordu. Şimdi her
üç yol da candidate'leri silmeden önce ilişkili ShortVideo satırlarını
(+ render edilmiş dosyalarını) temizliyor.

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

172 lines
6.2 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 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 updateNotificationPrefs(formData: FormData) {
const streamStart = formData.get("notifyStreamStart") === "on";
const renderDone = formData.get("notifyRenderDone") === "on";
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");
}