feat: interaktif panel kontrolleri (Faz 3)

Yayın durdurma, kanal duraklat/devam (tekli+toplu), şimdi kontrol et,
manuel URL ile zorla kayıt başlatma, canlı player+log önizleme, oturum
süre sayacı, başarısız/takılı render için tekrar dene ve iptal, poll
aralığı + ham segment TTL'yi panelden canlı değiştirme, cookie bayatlık
uyarısı, Telegram bildirim aç/kapa, ve kullanım/maliyet özeti (/stats).

api-daemon'ın yeni state-değiştiren endpoint'leri (stop/force-start/
render/cancel) INTERNAL_API_TOKEN ile korunuyor — bu daemon'ın portu
Coolify'de public'e açık olduğu için korumasız bırakmak güvenlik açığı
olurdu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 13:38:47 +03:00
co-authored by Claude Sonnet 5
parent 2fc3254d64
commit 43ccbd9ec7
23 changed files with 742 additions and 75 deletions
+21 -1
View File
@@ -1,4 +1,4 @@
import { spawn } from "node:child_process";
import { spawn, type ChildProcess } from "node:child_process";
import { mkdir, readFile } from "node:fs/promises";
import path from "node:path";
import { Worker, type Job } from "bullmq";
@@ -17,6 +17,24 @@ const SEGMENT_LIST_POLL_MS = 5_000;
*/
export const lastCaptureDebug = new Map<string, string[]>();
/**
* Live yt-dlp processes, keyed by sessionId — lets a panel "stop" action
* reach a specific in-progress capture. Only yt-dlp is tracked/killed:
* ffmpeg is downstream of it in the pipe, so ending yt-dlp closes its stdin
* with EOF and ffmpeg finishes the same way it does on a natural stream end
* (flushes the last partial segment, exits, existing finalize code runs
* unchanged).
*/
const activeCaptures = new Map<string, ChildProcess>();
/** Returns false if the session has no tracked live process (already ended, or this daemon didn't start it — e.g. after a restart). */
export function stopCapture(sessionId: string): boolean {
const ytdlp = activeCaptures.get(sessionId);
if (!ytdlp) return false;
ytdlp.kill("SIGTERM");
return true;
}
function appendCaptureDebug(sessionId: string, line: string) {
const lines = lastCaptureDebug.get(sessionId) ?? [];
lines.push(line);
@@ -95,6 +113,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
activeCaptures.set(sessionId, ytdlp);
const ffmpeg = spawn(
"ffmpeg",
@@ -153,6 +172,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
});
clearInterval(pollTimer);
activeCaptures.delete(sessionId);
// final sweep in case segments closed between the last poll and process exit
const raw = await readFile(segmentListPath, "utf8").catch(() => "");