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:
@@ -69,11 +69,22 @@ async function findLiveVideoId(channelId: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function startSession(channel: { id: string; name: string }, liveVideoId: string) {
|
||||
/**
|
||||
* Shared by the automatic poll loop and the panel's manual "force-start"
|
||||
* action — both just need a channel + a URL to begin capturing. A no-op if
|
||||
* that channel already has an open session (covers both callers: the poll
|
||||
* loop re-checking a channel it's already recording, and someone hitting
|
||||
* force-start on a channel that's already live).
|
||||
*/
|
||||
export async function startCaptureSession(
|
||||
channel: { id: string; name: string },
|
||||
youtubeUrl: string,
|
||||
liveVideoId: string | null = null,
|
||||
): Promise<{ started: boolean }> {
|
||||
const existing = await prisma.streamSession.findFirst({
|
||||
where: { channelId: channel.id, endedAt: null },
|
||||
});
|
||||
if (existing) return;
|
||||
if (existing) return { started: false };
|
||||
|
||||
const session = await prisma.streamSession.create({
|
||||
data: { channelId: channel.id, liveVideoId },
|
||||
@@ -82,11 +93,33 @@ async function startSession(channel: { id: string; name: string }, liveVideoId:
|
||||
await streamIngestQueue.add("start-capture", {
|
||||
sessionId: session.id,
|
||||
channelDbId: channel.id,
|
||||
youtubeUrl: `https://www.youtube.com/watch?v=${liveVideoId}`,
|
||||
youtubeUrl,
|
||||
});
|
||||
|
||||
await sendTelegramMessage(`🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`);
|
||||
const notifySetting = await prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } });
|
||||
if (notifySetting?.value !== "false") {
|
||||
await sendTelegramMessage(`🔴 *${channel.name}* canlıya geçti, kayıt başlatılıyor.`);
|
||||
}
|
||||
console.log(`[youtube-polling] started session ${session.id} for channel ${channel.name}`);
|
||||
return { started: true };
|
||||
}
|
||||
|
||||
/** Runs the live-check for a single channel and applies its result — the unit both `pollOnce` and the panel's manual "şimdi kontrol et" action reuse. */
|
||||
export async function checkChannel(channel: { id: string; name: string; channelId: string }): Promise<{
|
||||
liveVideoId: string | null;
|
||||
error: string | null;
|
||||
}> {
|
||||
const liveVideoId = await findLiveVideoId(channel.channelId);
|
||||
await prisma.channel.update({
|
||||
where: { id: channel.id },
|
||||
data: { lastCheckedAt: new Date() },
|
||||
});
|
||||
|
||||
if (liveVideoId) {
|
||||
await startCaptureSession(channel, `https://www.youtube.com/watch?v=${liveVideoId}`, liveVideoId);
|
||||
}
|
||||
|
||||
return { liveVideoId, error: lastPollErrors.get(channel.channelId) ?? null };
|
||||
}
|
||||
|
||||
export async function pollOnce(): Promise<void> {
|
||||
@@ -94,25 +127,39 @@ export async function pollOnce(): Promise<void> {
|
||||
|
||||
for (const channel of channels) {
|
||||
try {
|
||||
const liveVideoId = await findLiveVideoId(channel.channelId);
|
||||
await prisma.channel.update({
|
||||
where: { id: channel.id },
|
||||
data: { lastCheckedAt: new Date() },
|
||||
});
|
||||
|
||||
if (liveVideoId) {
|
||||
await startSession(channel, liveVideoId);
|
||||
}
|
||||
await checkChannel(channel);
|
||||
} catch (err) {
|
||||
console.error(`[youtube-polling] error polling channel ${channel.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function startPollingLoop(): NodeJS.Timeout {
|
||||
console.log(`[youtube-polling] polling every ${env.pollIntervalMs}ms`);
|
||||
pollOnce().catch((err) => console.error("[youtube-polling] initial poll failed:", err));
|
||||
return setInterval(() => {
|
||||
pollOnce().catch((err) => console.error("[youtube-polling] poll failed:", err));
|
||||
}, env.pollIntervalMs);
|
||||
const POLL_INTERVAL_SETTING_KEY = "poll_interval_ms";
|
||||
|
||||
async function getPollIntervalMs(): Promise<number> {
|
||||
const setting = await prisma.appSetting.findUnique({ where: { key: POLL_INTERVAL_SETTING_KEY } });
|
||||
const parsed = setting ? Number(setting.value) : NaN;
|
||||
return Number.isFinite(parsed) && parsed >= 5_000 ? parsed : env.pollIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-rescheduling instead of setInterval so a panel-side change to the
|
||||
* poll_interval_ms AppSetting (read fresh on every iteration, same pattern
|
||||
* as ytdlpAntiBotArgs()'s cookies) takes effect on the very next cycle
|
||||
* without a redeploy.
|
||||
*/
|
||||
export function startPollingLoop(): void {
|
||||
let stopped = false;
|
||||
|
||||
async function tick() {
|
||||
if (stopped) return;
|
||||
await pollOnce().catch((err) => console.error("[youtube-polling] poll failed:", err));
|
||||
const intervalMs = await getPollIntervalMs().catch(() => env.pollIntervalMs);
|
||||
if (!stopped) setTimeout(tick, intervalMs);
|
||||
}
|
||||
|
||||
getPollIntervalMs()
|
||||
.then((ms) => console.log(`[youtube-polling] polling every ${ms}ms`))
|
||||
.catch(() => {});
|
||||
tick();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user