feat: kanal başına segment süresini panelden ayarlama
Channel.segmentTimeSec (nullable, null = global SEGMENT_TIME_SEC env var'ı kullan) eklendi. Kanal detay sayfasından dakika cinsinden girilebiliyor, StreamIngestJob'a taşınıp streamIngest.ts'te ffmpeg'in -segment_time argümanına yansıyor. Sadece o kanal için bundan sonra başlayacak yeni kayıt oturumlarını etkiler — halihazırda çalışan bir ffmpeg process'inin segment süresi zaten sabitlenmiş durumda. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ async function processCompletedSegments(
|
||||
}
|
||||
|
||||
async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
||||
const { sessionId, youtubeUrl } = job.data;
|
||||
const { sessionId, youtubeUrl, segmentTimeSec } = job.data;
|
||||
const outDir = path.join(env.sharedMediaRoot, "raw", sessionId);
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
@@ -121,7 +121,7 @@ async function runCapture(job: Job<StreamIngestJob>): Promise<void> {
|
||||
"-i", "pipe:0",
|
||||
"-c", "copy",
|
||||
"-f", "segment",
|
||||
"-segment_time", String(env.segmentTimeSec),
|
||||
"-segment_time", String(segmentTimeSec ?? env.segmentTimeSec),
|
||||
"-reset_timestamps", "1",
|
||||
"-segment_list", segmentListPath,
|
||||
"-segment_list_type", "csv",
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface StreamIngestJob {
|
||||
sessionId: string;
|
||||
channelDbId: string;
|
||||
youtubeUrl: string;
|
||||
/** Kanala özel segment süresi (sn) — verilmezse env.segmentTimeSec kullanılır. */
|
||||
segmentTimeSec?: number;
|
||||
}
|
||||
|
||||
export interface SignalDetectionJob {
|
||||
|
||||
@@ -85,7 +85,7 @@ async function findLiveVideoId(channelId: string): Promise<string | null> {
|
||||
* force-start on a channel that's already live).
|
||||
*/
|
||||
export async function startCaptureSession(
|
||||
channel: { id: string; name: string },
|
||||
channel: { id: string; name: string; segmentTimeSec?: number | null },
|
||||
youtubeUrl: string,
|
||||
liveVideoId: string | null = null,
|
||||
): Promise<{ started: boolean }> {
|
||||
@@ -102,6 +102,7 @@ export async function startCaptureSession(
|
||||
sessionId: session.id,
|
||||
channelDbId: channel.id,
|
||||
youtubeUrl,
|
||||
segmentTimeSec: channel.segmentTimeSec ?? undefined,
|
||||
});
|
||||
|
||||
const notifySetting = await prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } });
|
||||
@@ -113,7 +114,12 @@ export async function startCaptureSession(
|
||||
}
|
||||
|
||||
/** 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<{
|
||||
export async function checkChannel(channel: {
|
||||
id: string;
|
||||
name: string;
|
||||
channelId: string;
|
||||
segmentTimeSec?: number | null;
|
||||
}): Promise<{
|
||||
liveVideoId: string | null;
|
||||
error: PollError | null;
|
||||
}> {
|
||||
|
||||
@@ -80,6 +80,15 @@ export async function toggleChannelActive(channelId: string, nextActive: boolean
|
||||
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");
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
forceStartCapture,
|
||||
retryRender,
|
||||
cancelRender,
|
||||
updateChannelSegmentTime,
|
||||
} from "../../actions";
|
||||
import { ConfirmButton } from "../../components/ConfirmButton";
|
||||
import { LiveLogViewer } from "../../components/LiveLogViewer";
|
||||
import { SessionTimer } from "../../components/SessionTimer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
@@ -114,6 +116,34 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
{channel.lastCheckedAt ? new Date(channel.lastCheckedAt).toLocaleString("tr-TR") : "—"}
|
||||
</p>
|
||||
|
||||
<form
|
||||
action={updateChannelSegmentTime.bind(null, channel.id)}
|
||||
className="flex flex-wrap items-end gap-2"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="segmentTimeMin" className="text-xs text-muted-foreground">
|
||||
Bu kanal için segment süresi (dakika)
|
||||
</Label>
|
||||
<Input
|
||||
id="segmentTimeMin"
|
||||
name="segmentTimeMin"
|
||||
type="number"
|
||||
min={1}
|
||||
max={60}
|
||||
step={0.5}
|
||||
placeholder="varsayılan (15)"
|
||||
defaultValue={channel.segmentTimeSec ? channel.segmentTimeSec / 60 : ""}
|
||||
className="w-40"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
Kaydet
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Boş bırakılırsa global varsayılan kullanılır. Sadece bundan sonra başlayacak kayıtları etkiler.
|
||||
</span>
|
||||
</form>
|
||||
|
||||
{status === undefined && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
|
||||
|
||||
Reference in New Issue
Block a user