Compare commits
3
Commits
eca80959fb
...
b5c761afbf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5c761afbf | ||
|
|
dea207996e | ||
|
|
58ef40edf8 |
+1
-1
@@ -12,7 +12,7 @@ TELEGRAM_BOT_TOKEN=
|
|||||||
TELEGRAM_CHAT_ID=
|
TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
POLL_INTERVAL_MS=60000
|
POLL_INTERVAL_MS=60000
|
||||||
SEGMENT_TIME_SEC=900
|
SEGMENT_TIME_SEC=300
|
||||||
API_DAEMON_PORT=4001
|
API_DAEMON_PORT=4001
|
||||||
|
|
||||||
# Signs the panel's login session cookies (frontend). Generate with:
|
# Signs the panel's login session cookies (frontend). Generate with:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const env = {
|
|||||||
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
|
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
|
||||||
sharedMediaRoot: process.env.SHARED_MEDIA_ROOT ?? "./shared-media",
|
sharedMediaRoot: process.env.SHARED_MEDIA_ROOT ?? "./shared-media",
|
||||||
pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 60_000),
|
pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 60_000),
|
||||||
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 900),
|
segmentTimeSec: Number(process.env.SEGMENT_TIME_SEC ?? 300),
|
||||||
port: Number(process.env.API_DAEMON_PORT ?? 4001),
|
port: Number(process.env.API_DAEMON_PORT ?? 4001),
|
||||||
forceLiveUrl: process.env.FORCE_LIVE_URL ?? "",
|
forceLiveUrl: process.env.FORCE_LIVE_URL ?? "",
|
||||||
ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "",
|
ytdlpPotProviderUrl: process.env.YTDLP_POT_PROVIDER_URL ?? "",
|
||||||
|
|||||||
@@ -7,4 +7,12 @@ COPY . .
|
|||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
RUN pnpm --filter @streamclipper/db exec prisma generate
|
RUN pnpm --filter @streamclipper/db exec prisma generate
|
||||||
|
|
||||||
CMD ["pnpm", "--filter", "@streamclipper/frontend", "dev"]
|
# DATABASE_URL isn't needed for real at build time (all pages are
|
||||||
|
# force-dynamic — nothing queries the DB during `next build`), but
|
||||||
|
# Prisma's client validates the connection string format at import, so a
|
||||||
|
# well-formed placeholder keeps the build step from failing. Coolify's
|
||||||
|
# actual DATABASE_URL is supplied as a runtime env var and overrides this.
|
||||||
|
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build"
|
||||||
|
RUN pnpm --filter @streamclipper/frontend build
|
||||||
|
|
||||||
|
CMD ["pnpm", "--filter", "@streamclipper/frontend", "start"]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
import { Badge, type badgeVariants } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import type { VariantProps } from "class-variance-authority";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -111,40 +112,12 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<p className="font-mono-num text-xs text-muted-foreground">
|
<p className="font-mono-num text-xs text-muted-foreground">
|
||||||
Aktif: {channel.isActive ? "evet" : "hayır"} · Son kontrol:{" "}
|
Aktif: {channel.isActive ? "evet" : "hayır"} · Son kontrol:{" "}
|
||||||
{channel.lastCheckedAt ? formatTr(channel.lastCheckedAt) : "—"}
|
{channel.lastCheckedAt ? formatTr(channel.lastCheckedAt) : "—"}
|
||||||
</p>
|
</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 && (
|
{status === undefined && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
|
api-daemon'dan canlı durum alınamadı (servis erişilemez olabilir).
|
||||||
@@ -152,7 +125,7 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{status?.lastPollError && (
|
{status?.lastPollError && (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5 rounded-lg border border-err/30 bg-err/5 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="err" className="w-fit">
|
<Badge variant="err" className="w-fit">
|
||||||
son poll hatası
|
son poll hatası
|
||||||
@@ -161,32 +134,64 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
{formatTr(status.lastPollError.at)}
|
{formatTr(status.lastPollError.at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<pre className="font-mono-num whitespace-pre-wrap rounded-md border border-border bg-background/60 p-3 text-[0.7rem] text-muted-foreground">
|
<pre className="font-mono-num whitespace-pre-wrap text-[0.7rem] text-muted-foreground">
|
||||||
{status.lastPollError.message}
|
{status.lastPollError.message}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status?.recording && activeSession?.liveVideoId && activeSession.liveVideoId !== "forced" && (
|
{status?.recording && (
|
||||||
<iframe
|
<div className="flex flex-col gap-3">
|
||||||
src={`https://www.youtube.com/embed/${activeSession.liveVideoId}`}
|
{activeSession?.liveVideoId && activeSession.liveVideoId !== "forced" && (
|
||||||
title="Canlı yayın önizleme"
|
<iframe
|
||||||
className="aspect-video w-full max-w-md rounded-md border border-border"
|
src={`https://www.youtube.com/embed/${activeSession.liveVideoId}`}
|
||||||
allow="autoplay; encrypted-media"
|
title="Canlı yayın önizleme"
|
||||||
/>
|
className="aspect-video w-full max-w-md rounded-md border border-border"
|
||||||
|
allow="autoplay; encrypted-media"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeSession && <LiveLogViewer sessionId={activeSession.id} />}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status?.recording && activeSession && <LiveLogViewer sessionId={activeSession.id} />}
|
<Separator />
|
||||||
|
|
||||||
<details className="mt-1 text-sm">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<summary className="cursor-pointer font-mono-num text-xs text-muted-foreground">
|
<form
|
||||||
Manuel URL ile kayıt başlat
|
action={updateChannelSegmentTime.bind(null, channel.id)}
|
||||||
</summary>
|
className="flex flex-wrap items-end gap-2"
|
||||||
<form action={forceStartCapture.bind(null, channel.id)} className="mt-2 flex gap-2">
|
>
|
||||||
<Input name="url" placeholder="https://www.youtube.com/watch?v=..." required className="flex-1" />
|
<div className="flex flex-col gap-1.5">
|
||||||
<Button type="submit">Başlat</Button>
|
<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"
|
||||||
|
defaultValue={channel.segmentTimeSec ? channel.segmentTimeSec / 60 : ""}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" variant="outline" size="sm">
|
||||||
|
Kaydet
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
|
||||||
|
<details className="text-sm">
|
||||||
|
<summary className="cursor-pointer font-mono-num text-xs text-muted-foreground">
|
||||||
|
Manuel URL ile kayıt başlat
|
||||||
|
</summary>
|
||||||
|
<form action={forceStartCapture.bind(null, channel.id)} className="mt-2 flex gap-2">
|
||||||
|
<Input name="url" placeholder="https://www.youtube.com/watch?v=..." required className="w-64" />
|
||||||
|
<Button type="submit">Başlat</Button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -213,11 +218,14 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
) : (
|
) : (
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{session.segments.map((segment) => (
|
{session.segments.map((segment) => (
|
||||||
<div key={segment.id} className="border-l-2 border-border pl-4">
|
<div key={segment.id} className="rounded-lg border border-border bg-muted/20 p-4">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">{segment.filePath}</span>
|
<span className="text-sm font-medium">
|
||||||
|
{segment.duration}sn segment · {formatTr(segment.startedAt)}
|
||||||
|
</span>
|
||||||
<Badge variant={STATUS_BADGE[segment.status] ?? "muted"}>{segment.status}</Badge>
|
<Badge variant={STATUS_BADGE[segment.status] ?? "muted"}>{segment.status}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="font-mono-num text-[0.7rem] text-muted-foreground">{segment.filePath}</span>
|
||||||
|
|
||||||
<video controls preload="metadata" className="mt-2 w-full max-w-md rounded-md border border-border">
|
<video controls preload="metadata" className="mt-2 w-full max-w-md rounded-md border border-border">
|
||||||
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
|
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
|
||||||
@@ -236,73 +244,77 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{segment.candidates.length === 0 && segment.status !== "PENDING" && (
|
{segment.candidates.length === 0 && segment.status !== "PENDING" && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-3 text-xs text-muted-foreground">
|
||||||
Bu segmentte aday klip bulunamadı (sinyal analizi ilginç bir an tespit etmedi).
|
Bu segmentte aday klip bulunamadı (sinyal analizi ilginç bir an tespit etmedi).
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{segment.candidates.map((c) => (
|
{segment.candidates.length > 0 && (
|
||||||
<div key={c.id} className="mt-3 border-l-2 border-border pl-4">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
{segment.candidates.map((c) => (
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">
|
<div key={c.id} className="rounded-md border border-border bg-background/60 p-3">
|
||||||
{c.startSec}s – {c.endSec}s
|
<div className="flex items-center justify-between gap-2">
|
||||||
{c.audioPeakScore != null && ` · peak ${c.audioPeakScore.toFixed(1)}dB`}
|
<span className="font-mono-num text-xs text-muted-foreground">
|
||||||
</span>
|
{c.startSec}s – {c.endSec}s
|
||||||
<Badge variant={STATUS_BADGE[c.status] ?? "muted"}>{c.status}</Badge>
|
{c.audioPeakScore != null && ` · peak ${c.audioPeakScore.toFixed(1)}dB`}
|
||||||
</div>
|
</span>
|
||||||
{transcriptPreview(c.transcriptJson) && (
|
<Badge variant={STATUS_BADGE[c.status] ?? "muted"}>{c.status}</Badge>
|
||||||
<p className="mt-1.5 text-sm leading-relaxed">{transcriptPreview(c.transcriptJson)}</p>
|
</div>
|
||||||
)}
|
{transcriptPreview(c.transcriptJson) && (
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed">{transcriptPreview(c.transcriptJson)}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{c.short && (
|
{c.short && (
|
||||||
<div className="mt-2 flex flex-col items-start gap-2">
|
<div className="mt-2 flex flex-col items-start gap-2">
|
||||||
<Badge variant={STATUS_BADGE[c.short.status] ?? "muted"}>9:16: {c.short.status}</Badge>
|
<Badge variant={STATUS_BADGE[c.short.status] ?? "muted"}>9:16: {c.short.status}</Badge>
|
||||||
{c.short.status === "READY" && (
|
{c.short.status === "READY" && (
|
||||||
<>
|
<>
|
||||||
<video controls preload="metadata" className="w-[220px] rounded-md border border-border">
|
<video controls preload="metadata" className="w-[220px] rounded-md border border-border">
|
||||||
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
|
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
<a href={`/api/shorts/${c.short.id}/video?download=1`}>İndir</a>
|
<a href={`/api/shorts/${c.short.id}/video?download=1`}>İndir</a>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
{c.short.status === "FAILED" && (
|
|
||||||
<>
|
|
||||||
{c.short.errorMessage && (
|
|
||||||
<p className="font-mono-num text-xs text-muted-foreground">{c.short.errorMessage}</p>
|
|
||||||
)}
|
)}
|
||||||
<form action={retryRender.bind(null, c.id)}>
|
{c.short.status === "FAILED" && (
|
||||||
<Button type="submit" size="sm">
|
<>
|
||||||
Tekrar Dene
|
{c.short.errorMessage && (
|
||||||
</Button>
|
<p className="font-mono-num text-xs text-muted-foreground">{c.short.errorMessage}</p>
|
||||||
</form>
|
)}
|
||||||
</>
|
<form action={retryRender.bind(null, c.id)}>
|
||||||
|
<Button type="submit" size="sm">
|
||||||
|
Tekrar Dene
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{c.short.status === "RENDERING" && (
|
||||||
|
<form action={cancelRender.bind(null, c.id)}>
|
||||||
|
<ConfirmButton
|
||||||
|
confirmText="Render'ı başarısız say ve kilidi aç? (Çalışan process'i öldürmez, sadece durumu sıfırlar)"
|
||||||
|
label="Takıldıysa İptal Et"
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{c.short.status === "RENDERING" && (
|
{!c.short && c.status === "TRANSCRIBED" && (
|
||||||
<form action={cancelRender.bind(null, c.id)}>
|
<form action={retryRender.bind(null, c.id)} className="mt-2">
|
||||||
<ConfirmButton
|
<Button type="submit" size="sm">
|
||||||
confirmText="Render'ı başarısız say ve kilidi aç? (Çalışan process'i öldürmez, sadece durumu sıfırlar)"
|
9:16 Render Et
|
||||||
label="Takıldıysa İptal Et"
|
</Button>
|
||||||
variant="outline"
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!c.short && c.status === "TRANSCRIBED" && (
|
|
||||||
<form action={retryRender.bind(null, c.id)} className="mt-2">
|
|
||||||
<Button type="submit" size="sm">
|
|
||||||
9:16 Render Et
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form action={deleteCandidateSegment.bind(null, c.id)} className="mt-2">
|
<form action={deleteCandidateSegment.bind(null, c.id)} className="mt-2">
|
||||||
<ConfirmButton confirmText="Bu aday klibi silmek istediğine emin misin?" label="Sil" />
|
<ConfirmButton confirmText="Bu aday klibi silmek istediğine emin misin?" label="Sil" />
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+64
-69
@@ -6,8 +6,8 @@ import { formatTr } from "../lib/formatDate";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -47,9 +47,9 @@ export default async function DashboardPage() {
|
|||||||
{channels.length === 0 ? (
|
{channels.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">Henüz kanal eklenmedi.</p>
|
<p className="text-sm text-muted-foreground">Henüz kanal eklenmedi.</p>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<div className="flex flex-col gap-3">
|
||||||
<CardHeader className="flex-row items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">{channels.length} kanal</CardTitle>
|
<span className="text-sm font-medium text-muted-foreground">{channels.length} kanal</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<form action={toggleAllChannels.bind(null, true)}>
|
<form action={toggleAllChannels.bind(null, true)}>
|
||||||
<Button type="submit" variant="outline" size="sm">
|
<Button type="submit" variant="outline" size="sm">
|
||||||
@@ -62,70 +62,65 @@ export default async function DashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
|
||||||
<Table>
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<TableHeader>
|
{channels.map((c) => {
|
||||||
<TableRow>
|
const recording = c.sessions.length > 0;
|
||||||
<TableHead>Kanal</TableHead>
|
const status = statuses.get(c.id);
|
||||||
<TableHead>Durum</TableHead>
|
return (
|
||||||
<TableHead>Kayıt</TableHead>
|
<Card
|
||||||
<TableHead>Son Kontrol</TableHead>
|
key={c.id}
|
||||||
<TableHead className="text-right">Aksiyon</TableHead>
|
className={cn("gap-4 py-5", recording && "border-live/40")}
|
||||||
</TableRow>
|
>
|
||||||
</TableHeader>
|
<CardHeader className="gap-1 px-5">
|
||||||
<TableBody>
|
<div className="flex items-start justify-between gap-2">
|
||||||
{channels.map((c) => {
|
<Link href={`/channels/${c.id}`} className="font-semibold leading-tight hover:text-primary hover:underline">
|
||||||
const recording = c.sessions.length > 0;
|
{c.name}
|
||||||
const status = statuses.get(c.id);
|
</Link>
|
||||||
return (
|
<Badge variant={c.isActive ? "ok" : "muted"} className="shrink-0">
|
||||||
<TableRow key={c.id}>
|
{c.isActive ? "Aktif" : "Pasif"}
|
||||||
<TableCell>
|
</Badge>
|
||||||
<Link href={`/channels/${c.id}`} className="font-medium hover:text-primary hover:underline">
|
</div>
|
||||||
{c.name}
|
<span className="font-mono-num text-xs text-muted-foreground">{c.youtubeHandle}</span>
|
||||||
</Link>{" "}
|
</CardHeader>
|
||||||
<span className="font-mono-num text-xs text-muted-foreground">{c.youtubeHandle}</span>
|
|
||||||
</TableCell>
|
<CardContent className="flex flex-col gap-2 px-5">
|
||||||
<TableCell>
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<Badge variant={c.isActive ? "ok" : "muted"}>{c.isActive ? "Aktif" : "Pasif"}</Badge>
|
<Badge variant={recording ? "live" : "muted"}>
|
||||||
</TableCell>
|
{recording ? "🔴 Kayıtta" : "Kayıtta değil"}
|
||||||
<TableCell>
|
</Badge>
|
||||||
<div className="flex items-center gap-1.5">
|
{status?.lastPollError && (
|
||||||
<Badge variant={recording ? "live" : "muted"}>{recording ? "🔴 Kayıtta" : "—"}</Badge>
|
<Badge
|
||||||
{status?.lastPollError && (
|
variant="err"
|
||||||
<Badge
|
title={`${formatTr(status.lastPollError.at)} — ${status.lastPollError.message}`}
|
||||||
variant="err"
|
>
|
||||||
title={`${formatTr(status.lastPollError.at)} — ${status.lastPollError.message}`}
|
hata
|
||||||
>
|
</Badge>
|
||||||
hata
|
)}
|
||||||
</Badge>
|
</div>
|
||||||
)}
|
<span className="font-mono-num text-xs text-muted-foreground">
|
||||||
</div>
|
Son kontrol: {c.lastCheckedAt ? formatTr(c.lastCheckedAt) : "—"}
|
||||||
</TableCell>
|
</span>
|
||||||
<TableCell className="font-mono-num text-xs text-muted-foreground">
|
</CardContent>
|
||||||
{c.lastCheckedAt ? formatTr(c.lastCheckedAt) : "—"}
|
|
||||||
</TableCell>
|
<CardFooter className="gap-2 px-5">
|
||||||
<TableCell>
|
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
|
||||||
<div className="flex justify-end gap-1.5">
|
<Button type="submit" variant="outline" size="sm">
|
||||||
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
|
{c.isActive ? "Duraklat" : "Devam Ettir"}
|
||||||
<Button type="submit" variant="outline" size="sm">
|
</Button>
|
||||||
{c.isActive ? "Duraklat" : "Devam Ettir"}
|
</form>
|
||||||
</Button>
|
<form action={checkChannelNow.bind(null, c.id)}>
|
||||||
</form>
|
<Button type="submit" variant="ghost" size="sm">
|
||||||
<form action={checkChannelNow.bind(null, c.id)}>
|
Şimdi Kontrol Et
|
||||||
<Button type="submit" variant="ghost" size="sm">
|
</Button>
|
||||||
Şimdi Kontrol Et
|
</form>
|
||||||
</Button>
|
</CardFooter>
|
||||||
</form>
|
</Card>
|
||||||
</div>
|
);
|
||||||
</TableCell>
|
})}
|
||||||
</TableRow>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ WINDOW_SEC = 1.0
|
|||||||
PEAK_STD_MULTIPLIER = 1.5
|
PEAK_STD_MULTIPLIER = 1.5
|
||||||
CANDIDATE_PAD_SEC = 45
|
CANDIDATE_PAD_SEC = 45
|
||||||
CHAT_SPIKE_MULTIPLIER = 3
|
CHAT_SPIKE_MULTIPLIER = 3
|
||||||
SEGMENT_TIME_SEC = int(os.environ.get("SEGMENT_TIME_SEC", "900"))
|
SEGMENT_TIME_SEC = int(os.environ.get("SEGMENT_TIME_SEC", "300"))
|
||||||
|
|
||||||
stt_scoring_queue = Queue(QUEUE_NAMES["STT_SCORING"], {"connection": REDIS_URL})
|
stt_scoring_queue = Queue(QUEUE_NAMES["STT_SCORING"], {"connection": REDIS_URL})
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -33,7 +33,7 @@ services:
|
|||||||
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
|
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
|
||||||
SHARED_MEDIA_ROOT: /shared-media
|
SHARED_MEDIA_ROOT: /shared-media
|
||||||
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-60000}
|
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-60000}
|
||||||
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-900}
|
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-300}
|
||||||
API_DAEMON_PORT: ${API_DAEMON_PORT:-4001}
|
API_DAEMON_PORT: ${API_DAEMON_PORT:-4001}
|
||||||
FORCE_LIVE_URL: ${FORCE_LIVE_URL:-}
|
FORCE_LIVE_URL: ${FORCE_LIVE_URL:-}
|
||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
REDIS_URL: redis://sc_redis:6379
|
REDIS_URL: redis://sc_redis:6379
|
||||||
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
|
DATABASE_URL: postgresql://streamclipper:streamclipper@sc_postgres:5432/streamclipper
|
||||||
SHARED_MEDIA_ROOT: /shared-media
|
SHARED_MEDIA_ROOT: /shared-media
|
||||||
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-900}
|
SEGMENT_TIME_SEC: ${SEGMENT_TIME_SEC:-300}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
|
|||||||
Reference in New Issue
Block a user