Files
screenclipper/apps/frontend/app/actions.ts
T
ayrisdevandClaude Sonnet 5 a53f0a5e04 feat: canlı-tespit hatası artık Telegram'a düşüyor + zaman damgası ekle
Poll hatası şu ana kadar sadece panele girip bakınca görülüyordu — proaktif
haberdar olma yolu yoktu. Şimdi bir kanal hata vermeye BAŞLADIĞINDA (her
60sn'de tekrar değil, sadece durum değişince) ve düzeldiğinde Telegram
bildirimi gidiyor. Ayarlar'dan kapatılabilir (notify_poll_error).

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

197 lines
7.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 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 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");
}