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") : "—"}

+
+
+ + +
+ + + Boş bırakılırsa global varsayılan kullanılır. Sadece bundan sonra başlayacak kayıtları etkiler. + +
+ {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[]