diff --git a/apps/api-daemon/src/capture/streamIngest.ts b/apps/api-daemon/src/capture/streamIngest.ts
index 405de12..873d7e8 100644
--- a/apps/api-daemon/src/capture/streamIngest.ts
+++ b/apps/api-daemon/src/capture/streamIngest.ts
@@ -96,7 +96,7 @@ async function processCompletedSegments(
}
async function runCapture(job: Job): Promise {
- 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): Promise {
"-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",
diff --git a/apps/api-daemon/src/queues.ts b/apps/api-daemon/src/queues.ts
index d0280a2..b57d039 100644
--- a/apps/api-daemon/src/queues.ts
+++ b/apps/api-daemon/src/queues.ts
@@ -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 {
diff --git a/apps/api-daemon/src/youtubePolling.ts b/apps/api-daemon/src/youtubePolling.ts
index c1f96f6..85f7ce4 100644
--- a/apps/api-daemon/src/youtubePolling.ts
+++ b/apps/api-daemon/src/youtubePolling.ts
@@ -85,7 +85,7 @@ async function findLiveVideoId(channelId: string): Promise {
* 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;
}> {
diff --git a/apps/frontend/app/actions.ts b/apps/frontend/app/actions.ts
index b31e410..9026772 100644
--- a/apps/frontend/app/actions.ts
+++ b/apps/frontend/app/actions.ts
@@ -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");
diff --git a/apps/frontend/app/channels/[id]/page.tsx b/apps/frontend/app/channels/[id]/page.tsx
index 2fc4131..e533415 100644
--- a/apps/frontend/app/channels/[id]/page.tsx
+++ b/apps/frontend/app/channels/[id]/page.tsx
@@ -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") : "—"}
+
+
{status === undefined && (
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
diff --git a/packages/db/prisma/migrations/20260902000000_channel_segment_time/migration.sql b/packages/db/prisma/migrations/20260902000000_channel_segment_time/migration.sql
new file mode 100644
index 0000000..63eeb10
--- /dev/null
+++ b/packages/db/prisma/migrations/20260902000000_channel_segment_time/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "channels" ADD COLUMN "segment_time_sec" INTEGER;
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index cf9a7c3..159f04e 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -34,6 +34,8 @@ model Channel {
isActive Boolean @default(true) @map("is_active")
lastCheckedAt DateTime? @map("last_checked_at")
createdAt DateTime @default(now()) @map("created_at")
+ // null = global varsayılanı (SEGMENT_TIME_SEC env var) kullan
+ segmentTimeSec Int? @map("segment_time_sec")
sessions StreamSession[]