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>
254 lines
9.2 KiB
TypeScript
254 lines
9.2 KiB
TypeScript
"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 } });
|
||
if (segment.filePath) 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)).filter((p): p is string => Boolean(p));
|
||
|
||
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 updateSendRawSegmentsToTelegram(formData: FormData) {
|
||
const enabled = formData.get("sendRawSegmentsToTelegram") === "true";
|
||
await prisma.appSetting.upsert({
|
||
where: { key: "send_raw_segments_to_telegram" },
|
||
create: { key: "send_raw_segments_to_telegram", 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");
|
||
}
|
||
|
||
export async function addCookieProfile(formData: FormData) {
|
||
const label = String(formData.get("label") ?? "").trim();
|
||
const value = String(formData.get("value") ?? "").trim();
|
||
|
||
if (!label || !value) {
|
||
throw new Error("Hesap adı ve cookie içeriği zorunlu.");
|
||
}
|
||
|
||
await prisma.cookieProfile.create({ data: { label, value } });
|
||
revalidatePath("/settings");
|
||
}
|
||
|
||
export async function updateCookieProfile(profileId: string, formData: FormData) {
|
||
const value = String(formData.get("value") ?? "").trim();
|
||
if (!value) {
|
||
throw new Error("Cookie içeriği boş olamaz.");
|
||
}
|
||
|
||
await prisma.cookieProfile.update({ where: { id: profileId }, data: { value } });
|
||
revalidatePath("/settings");
|
||
}
|
||
|
||
export async function deleteCookieProfile(profileId: string) {
|
||
await prisma.cookieProfile.delete({ where: { id: profileId } });
|
||
revalidatePath("/settings");
|
||
}
|