Show IPTV/Live TV under admin active streams without app changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6a637ab2ef
commit
a7a7c9dec4
6 changed files with 386 additions and 39 deletions
|
|
@ -5,6 +5,7 @@ import { Nav, useAuth } from "@/components/Nav";
|
|||
|
||||
interface Stream {
|
||||
id: string;
|
||||
source?: "library" | "iptv";
|
||||
node: string;
|
||||
status: string;
|
||||
viewer: string;
|
||||
|
|
@ -81,7 +82,8 @@ export default function StreamsPage() {
|
|||
</div>
|
||||
|
||||
<p className="muted page-lead">
|
||||
Eén rij per kijker. Stopt iemand → verdwijnt binnen ~3 min. Voortgang ≈ bytes / bestand.
|
||||
Bibliotheek én Live TV / IPTV. Bibliotheek verdwijnt binnen ~3 min na stoppen; IPTV
|
||||
blijft zolang Xtream een actieve connectie ziet.
|
||||
</p>
|
||||
|
||||
<div className="desktop-table card">
|
||||
|
|
@ -99,6 +101,7 @@ export default function StreamsPage() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{streams.map((s) => {
|
||||
const isIptv = s.source === "iptv" || s.node === "IPTV";
|
||||
const pct = s.progressPercent ?? 0;
|
||||
return (
|
||||
<tr key={s.id}>
|
||||
|
|
@ -107,23 +110,33 @@ export default function StreamsPage() {
|
|||
</td>
|
||||
<td>
|
||||
<div>{s.title}</div>
|
||||
<div className="muted tiny">{formatBytes(s.bytesSent)} gestreamd</div>
|
||||
<div className="muted tiny">
|
||||
{isIptv ? "Direct naar Xtream" : `${formatBytes(s.bytesSent)} gestreamd`}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ minWidth: 140 }}>
|
||||
{isIptv ? (
|
||||
<div className="muted mono tiny">live</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="progress-track" title={`${pct.toFixed(0)}%`}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="muted mono tiny">
|
||||
{s.progressPercent != null ? `${pct.toFixed(0)}%` : "—"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td>{s.node}</td>
|
||||
<td>{s.resolution ?? "-"}</td>
|
||||
<td className="mono tiny">{relativeTime(s.lastActivity)}</td>
|
||||
<td>
|
||||
{!isIptv && (
|
||||
<button type="button" className="danger" onClick={() => revoke(s.id)}>
|
||||
Intrekken
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
@ -141,6 +154,7 @@ export default function StreamsPage() {
|
|||
|
||||
<div className="list-cards">
|
||||
{streams.map((s) => {
|
||||
const isIptv = s.source === "iptv" || s.node === "IPTV";
|
||||
const pct = s.progressPercent ?? 0;
|
||||
return (
|
||||
<article key={s.id} className="list-card stream-card">
|
||||
|
|
@ -149,23 +163,28 @@ export default function StreamsPage() {
|
|||
<span className="mono tiny">{relativeTime(s.lastActivity)}</span>
|
||||
</div>
|
||||
<div className="stream-title">{s.title}</div>
|
||||
{!isIptv && (
|
||||
<div className="progress-track" title={`${pct.toFixed(0)}%`}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="list-card-meta">
|
||||
<span className="mono">
|
||||
{s.progressPercent != null ? `${pct.toFixed(0)}%` : "—"} ·{" "}
|
||||
{formatBytes(s.bytesSent)}
|
||||
{isIptv
|
||||
? "live · Xtream"
|
||||
: `${s.progressPercent != null ? `${pct.toFixed(0)}%` : "—"} · ${formatBytes(s.bytesSent)}`}
|
||||
</span>
|
||||
<span>
|
||||
{s.resolution ?? "?"} · {s.node}
|
||||
</span>
|
||||
</div>
|
||||
{!isIptv && (
|
||||
<div className="action-row">
|
||||
<button type="button" className="danger" onClick={() => revoke(s.id)}>
|
||||
Intrekken
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { AppError } from "../security/errors";
|
|||
import { nodeConnectionManager } from "../websocket/manager";
|
||||
import { MetadataService } from "../metadata/service";
|
||||
import type { Config } from "../config";
|
||||
import { listActiveIptvWatches } from "../iptv/active-watches";
|
||||
|
||||
export async function registerAdminRoutes(app: FastifyInstance, config: Config) {
|
||||
const metadata = new MetadataService(config.TMDB_API_KEY);
|
||||
|
|
@ -38,7 +39,8 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
prisma.movie.count(),
|
||||
prisma.series.count(),
|
||||
prisma.episode.count(),
|
||||
prisma.playbackSession.count({
|
||||
(async () => {
|
||||
const library = await prisma.playbackSession.count({
|
||||
where: {
|
||||
status: "ACTIVE",
|
||||
revoked: false,
|
||||
|
|
@ -46,7 +48,10 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
lastActivity: { gt: new Date(Date.now() - 3 * 60 * 1000) },
|
||||
bytesSent: { gt: 512n * 1024n },
|
||||
},
|
||||
}),
|
||||
});
|
||||
const iptv = await listActiveIptvWatches(config);
|
||||
return library + iptv.length;
|
||||
})(),
|
||||
prisma.node.findMany({
|
||||
where: { revoked: false, status: "ONLINE" },
|
||||
select: {
|
||||
|
|
@ -291,8 +296,15 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
take: 100,
|
||||
});
|
||||
|
||||
const iptvWatches = await listActiveIptvWatches(config);
|
||||
|
||||
// App-clients zetten ViewerUser.id in userId (zonder addonToken).
|
||||
const viewerIds = [...new Set(sessions.map((s) => s.userId).filter((id): id is string => !!id))];
|
||||
const viewerIds = [
|
||||
...new Set([
|
||||
...sessions.map((s) => s.userId).filter((id): id is string => !!id),
|
||||
...iptvWatches.map((w) => w.viewerUserId).filter((id): id is string => !!id),
|
||||
]),
|
||||
];
|
||||
const viewerUsers =
|
||||
viewerIds.length > 0
|
||||
? await prisma.viewerUser.findMany({
|
||||
|
|
@ -302,6 +314,22 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
: [];
|
||||
const viewerById = new Map(viewerUsers.map((v) => [v.id, v]));
|
||||
|
||||
const tokenIds = [
|
||||
...new Set(iptvWatches.map((w) => w.addonTokenId).filter((id): id is string => !!id)),
|
||||
];
|
||||
const tokens =
|
||||
tokenIds.length > 0
|
||||
? await prisma.addonToken.findMany({
|
||||
where: { id: { in: tokenIds } },
|
||||
select: {
|
||||
id: true,
|
||||
label: true,
|
||||
viewerUser: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
})
|
||||
: [];
|
||||
const tokenById = new Map(tokens.map((t) => [t.id, t]));
|
||||
|
||||
function streamViewerLabel(s: (typeof sessions)[number]): string {
|
||||
const tokenLabel = s.addonToken?.label?.trim();
|
||||
if (tokenLabel) return tokenLabel;
|
||||
|
|
@ -316,6 +344,22 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
return "Onbekend";
|
||||
}
|
||||
|
||||
function iptvViewerLabel(w: (typeof iptvWatches)[number]): string {
|
||||
if (w.viewerUserId) {
|
||||
const v = viewerById.get(w.viewerUserId);
|
||||
if (v) return v.name?.trim() || v.email;
|
||||
}
|
||||
if (w.addonTokenId) {
|
||||
const t = tokenById.get(w.addonTokenId);
|
||||
const label =
|
||||
t?.label?.trim() ||
|
||||
t?.viewerUser?.name?.trim() ||
|
||||
t?.viewerUser?.email?.trim();
|
||||
if (label) return label;
|
||||
}
|
||||
return "Onbekend";
|
||||
}
|
||||
|
||||
// Eén rij per kijker+titel (hoogste bytes wint) — niet per mediabestand.
|
||||
const best = new Map<string, (typeof sessions)[number]>();
|
||||
for (const s of sessions) {
|
||||
|
|
@ -330,8 +374,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
if (!prev || s.bytesSent > prev.bytesSent) best.set(key, s);
|
||||
}
|
||||
|
||||
return {
|
||||
streams: [...best.values()]
|
||||
const libraryStreams = [...best.values()]
|
||||
.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime())
|
||||
.map((s) => {
|
||||
const fileSize = Number(s.fileSizeBytes || s.mediaFile.sizeBytes || 0);
|
||||
|
|
@ -347,6 +390,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
const year = s.mediaFile.movie?.year ?? null;
|
||||
return {
|
||||
id: s.id,
|
||||
source: "library" as const,
|
||||
node: s.node.name,
|
||||
status: s.status,
|
||||
viewer: streamViewerLabel(s),
|
||||
|
|
@ -361,12 +405,45 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
fileSizeBytes: (s.fileSizeBytes > 0n ? s.fileSizeBytes : s.mediaFile.sizeBytes).toString(),
|
||||
progressPercent,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const iptvStreams = iptvWatches.map((w) => {
|
||||
const kindLabel = w.kind === "live" ? "Live TV" : w.kind === "vod" ? "IPTV VOD" : "IPTV Series";
|
||||
return {
|
||||
id: w.id,
|
||||
source: "iptv" as const,
|
||||
node: "IPTV",
|
||||
status: "ACTIVE",
|
||||
viewer: iptvViewerLabel(w),
|
||||
addonTokenId: w.addonTokenId,
|
||||
createdAt: w.startedAt,
|
||||
lastActivity: w.lastActivity,
|
||||
absoluteExpiresAt: new Date(w.lastActivity.getTime() + 60 * 60_000),
|
||||
title: `${kindLabel} · ${w.title}`,
|
||||
resolution: null as string | null,
|
||||
bytesSent: "0",
|
||||
lastByteOffset: "0",
|
||||
fileSizeBytes: "0",
|
||||
progressPercent: null as number | null,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
streams: [...libraryStreams, ...iptvStreams].sort(
|
||||
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/api/v1/admin/streams/:id/revoke", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
if (id.startsWith("iptv-")) {
|
||||
throw new AppError(
|
||||
"INVALID_REQUEST",
|
||||
"IPTV-streams lopen direct naar Xtream en kunnen hier niet worden ingetrokken",
|
||||
400
|
||||
);
|
||||
}
|
||||
const session = await prisma.playbackSession.findUnique({ where: { id } });
|
||||
if (!session) throw new AppError("NOT_FOUND", "Session not found", 404);
|
||||
|
||||
|
|
|
|||
222
apps/master-api/src/iptv/active-watches.ts
Normal file
222
apps/master-api/src/iptv/active-watches.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { randomUUID } from "crypto";
|
||||
import type { Config } from "../config";
|
||||
import { prisma } from "../database/client";
|
||||
import { loadXtreamByLineId } from "./service";
|
||||
|
||||
export type IptvWatchKind = "live" | "vod" | "series";
|
||||
|
||||
export type IptvActiveWatch = {
|
||||
id: string;
|
||||
iptvLineId: string;
|
||||
viewerUserId: string | null;
|
||||
addonTokenId: string | null;
|
||||
kind: IptvWatchKind;
|
||||
streamId: number;
|
||||
title: string;
|
||||
startedAt: Date;
|
||||
lastActivity: Date;
|
||||
};
|
||||
|
||||
/** Eén actieve IPTV-kijk per lijn (nieuwste zender wint). */
|
||||
const byLineId = new Map<string, IptvActiveWatch>();
|
||||
|
||||
/** Cache Xtream active_cons per lijn (admin pollt elke 5s). */
|
||||
const consCache = new Map<string, { at: number; active: number }>();
|
||||
const CONS_CACHE_MS = 20_000;
|
||||
/** Altijd tonen na play, ook als XUI de connectie nog niet telt. */
|
||||
const GRACE_MS = 3 * 60_000;
|
||||
/** Hard maximum zonder enige activiteit / connectie. */
|
||||
const MAX_AGE_MS = 6 * 60 * 60_000;
|
||||
|
||||
function viewerKey(viewerUserId: string | null, addonTokenId: string | null): string {
|
||||
return viewerUserId ?? addonTokenId ?? "anon";
|
||||
}
|
||||
|
||||
export function touchIptvWatch(input: {
|
||||
iptvLineId: string;
|
||||
viewerUserId?: string | null;
|
||||
addonTokenId?: string | null;
|
||||
kind: IptvWatchKind;
|
||||
streamId: number;
|
||||
title: string;
|
||||
}): IptvActiveWatch {
|
||||
const now = new Date();
|
||||
const existing = byLineId.get(input.iptvLineId);
|
||||
const sameChannel =
|
||||
existing &&
|
||||
existing.streamId === input.streamId &&
|
||||
existing.kind === input.kind &&
|
||||
viewerKey(existing.viewerUserId, existing.addonTokenId) ===
|
||||
viewerKey(input.viewerUserId ?? null, input.addonTokenId ?? null);
|
||||
|
||||
const watch: IptvActiveWatch = {
|
||||
id: sameChannel ? existing.id : `iptv-${randomUUID()}`,
|
||||
iptvLineId: input.iptvLineId,
|
||||
viewerUserId: input.viewerUserId ?? existing?.viewerUserId ?? null,
|
||||
addonTokenId: input.addonTokenId ?? existing?.addonTokenId ?? null,
|
||||
kind: input.kind,
|
||||
streamId: input.streamId,
|
||||
title: input.title.trim() || existing?.title || `IPTV #${input.streamId}`,
|
||||
startedAt: sameChannel ? existing.startedAt : now,
|
||||
lastActivity: now,
|
||||
};
|
||||
byLineId.set(input.iptvLineId, watch);
|
||||
return watch;
|
||||
}
|
||||
|
||||
export function refreshIptvWatch(opts: {
|
||||
iptvLineId: string;
|
||||
streamId?: number;
|
||||
title?: string;
|
||||
}): void {
|
||||
const w = byLineId.get(opts.iptvLineId);
|
||||
if (!w) return;
|
||||
if (opts.streamId != null && w.streamId !== opts.streamId) return;
|
||||
w.lastActivity = new Date();
|
||||
if (opts.title?.trim()) w.title = opts.title.trim();
|
||||
}
|
||||
|
||||
async function activeConsForLine(lineId: string, config: Config): Promise<number | null> {
|
||||
const cached = consCache.get(lineId);
|
||||
if (cached && Date.now() - cached.at < CONS_CACHE_MS) return cached.active;
|
||||
|
||||
const xt = await loadXtreamByLineId(lineId, config);
|
||||
if (!xt) return null;
|
||||
try {
|
||||
const info = await xt.client.authenticate();
|
||||
const n = parseInt(String(info.activeConnections ?? "0"), 10);
|
||||
const active = Number.isFinite(n) ? n : 0;
|
||||
consCache.set(lineId, { at: Date.now(), active });
|
||||
return active;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actieve IPTV-kijks voor Admin Streams.
|
||||
* Na play: grace-periode. Daarna: aanhouden zolang Xtream active_cons > 0.
|
||||
*/
|
||||
export async function listActiveIptvWatches(config: Config): Promise<IptvActiveWatch[]> {
|
||||
const now = Date.now();
|
||||
const out: IptvActiveWatch[] = [];
|
||||
|
||||
for (const [lineId, watch] of [...byLineId.entries()]) {
|
||||
const age = now - watch.lastActivity.getTime();
|
||||
if (age > MAX_AGE_MS) {
|
||||
byLineId.delete(lineId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (age <= GRACE_MS) {
|
||||
out.push(watch);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cons = await activeConsForLine(lineId, config);
|
||||
if (cons != null && cons > 0) {
|
||||
watch.lastActivity = new Date();
|
||||
out.push(watch);
|
||||
continue;
|
||||
}
|
||||
if (cons === 0) {
|
||||
byLineId.delete(lineId);
|
||||
continue;
|
||||
}
|
||||
// Xtream onbereikbaar: nog even tonen
|
||||
if (age <= 15 * 60_000) out.push(watch);
|
||||
else byLineId.delete(lineId);
|
||||
}
|
||||
|
||||
return out.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime());
|
||||
}
|
||||
|
||||
/** Aantal watches in grace (dashboard-fallback zonder Xtream-check). */
|
||||
export function countActiveIptvWatches(): number {
|
||||
const now = Date.now();
|
||||
let n = 0;
|
||||
for (const w of byLineId.values()) {
|
||||
if (now - w.lastActivity.getTime() <= MAX_AGE_MS) n += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Stremio-addon: registreer kijk na stream-resolve. */
|
||||
export async function touchIptvWatchForToken(
|
||||
tokenId: string,
|
||||
kind: IptvWatchKind,
|
||||
streamId: number,
|
||||
title: string
|
||||
): Promise<void> {
|
||||
const token = await prisma.addonToken.findUnique({
|
||||
where: { id: tokenId },
|
||||
select: {
|
||||
id: true,
|
||||
viewerUserId: true,
|
||||
iptvLine: { select: { id: true } },
|
||||
},
|
||||
});
|
||||
if (!token) return;
|
||||
|
||||
let lineId = token.iptvLine?.id ?? null;
|
||||
if (!lineId && token.viewerUserId) {
|
||||
const line = await prisma.iptvLine.findUnique({
|
||||
where: { viewerUserId: token.viewerUserId },
|
||||
select: { id: true },
|
||||
});
|
||||
lineId = line?.id ?? null;
|
||||
}
|
||||
if (!lineId) return;
|
||||
|
||||
touchIptvWatch({
|
||||
iptvLineId: lineId,
|
||||
viewerUserId: token.viewerUserId,
|
||||
addonTokenId: token.id,
|
||||
kind,
|
||||
streamId,
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
/** App-client: registreer kijk na /play. */
|
||||
export async function touchIptvWatchForViewer(
|
||||
viewerId: string,
|
||||
kind: IptvWatchKind,
|
||||
streamId: number,
|
||||
title: string
|
||||
): Promise<void> {
|
||||
const line = await prisma.iptvLine.findUnique({
|
||||
where: { viewerUserId: viewerId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!line) {
|
||||
// Legacy: lijn via iptvAddonTokenId
|
||||
const viewer = await prisma.viewerUser.findUnique({
|
||||
where: { id: viewerId },
|
||||
select: { iptvAddonTokenId: true },
|
||||
});
|
||||
if (!viewer?.iptvAddonTokenId) return;
|
||||
const legacy = await prisma.iptvLine.findUnique({
|
||||
where: { addonTokenId: viewer.iptvAddonTokenId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!legacy) return;
|
||||
touchIptvWatch({
|
||||
iptvLineId: legacy.id,
|
||||
viewerUserId: viewerId,
|
||||
addonTokenId: viewer.iptvAddonTokenId,
|
||||
kind,
|
||||
streamId,
|
||||
title,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
touchIptvWatch({
|
||||
iptvLineId: line.id,
|
||||
viewerUserId: viewerId,
|
||||
kind,
|
||||
streamId,
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
|
@ -516,6 +516,9 @@ export async function getIptvStreams(
|
|||
// Real live HLS often hangs on XUI /auth/*.m3u8; MPEG-TS is reliable.
|
||||
if (type === "tv" && parsed.kind === "live") {
|
||||
const sid = parsed.streamId;
|
||||
const liveList = await xt.client.getLiveStreams();
|
||||
const channel = liveList.find((s) => s.stream_id === sid);
|
||||
const channelName = channel?.name?.trim() || `Live #${sid}`;
|
||||
const resolved = await xt.client.resolveLivePlayUrls(sid);
|
||||
const out: Array<{
|
||||
name: string;
|
||||
|
|
@ -533,7 +536,7 @@ export async function getIptvStreams(
|
|||
if (resolved.ts) {
|
||||
out.push({
|
||||
name: "Live · MPEG-TS",
|
||||
title: "XUI · direct ts",
|
||||
title: channelName,
|
||||
url: resolved.ts,
|
||||
behaviorHints: hint(`live-${sid}.ts`),
|
||||
});
|
||||
|
|
@ -541,7 +544,7 @@ export async function getIptvStreams(
|
|||
if (resolved.hls && resolved.hls !== resolved.ts) {
|
||||
out.push({
|
||||
name: "Live · HLS",
|
||||
title: "XUI · m3u8 (kan hangen)",
|
||||
title: channelName,
|
||||
url: resolved.hls,
|
||||
behaviorHints: hint(`live-${sid}.m3u8`),
|
||||
});
|
||||
|
|
@ -552,7 +555,7 @@ export async function getIptvStreams(
|
|||
const isTs = url.endsWith(".ts");
|
||||
out.push({
|
||||
name: isTs ? "Live · MPEG-TS" : "Live · HLS",
|
||||
title: "classic Xtream",
|
||||
title: channelName,
|
||||
url,
|
||||
behaviorHints: hint(isTs ? `live-${sid}.ts` : `live-${sid}.m3u8`),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -284,6 +284,20 @@ export async function registerStremioRoutes(app: FastifyInstance, config: Config
|
|||
{ type, id, streamCount: streams.length, sampleHost: safeStreamHost(streams[0]?.url) },
|
||||
"iptv streams resolved (direct to Xtream)"
|
||||
);
|
||||
const parsed = parseIptvId(id);
|
||||
if (parsed && streams.length > 0) {
|
||||
const kind =
|
||||
parsed.kind === "live" ? "live" : parsed.kind === "vod" ? "vod" : "series";
|
||||
const title =
|
||||
streams[0]?.title ||
|
||||
streams[0]?.name ||
|
||||
(parsed.kind === "live" ? `Live #${parsed.streamId}` : `IPTV #${parsed.streamId}`);
|
||||
void import("../iptv/active-watches")
|
||||
.then(({ touchIptvWatchForToken }) =>
|
||||
touchIptvWatchForToken(auth.tokenId, kind, parsed.streamId, title)
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
return { streams };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@ export async function getViewerIptvPlayUrl(
|
|||
const formatFor = (url: string): "hls" | "ts" =>
|
||||
url.includes(".m3u8") || url.includes("m3u8") ? "hls" : "ts";
|
||||
|
||||
void import("../iptv/active-watches")
|
||||
.then(({ touchIptvWatchForViewer }) =>
|
||||
touchIptvWatchForViewer(viewerId, "live", streamId, ch.name)
|
||||
)
|
||||
.catch(() => undefined);
|
||||
|
||||
return {
|
||||
streamId,
|
||||
name: ch.name,
|
||||
|
|
@ -228,6 +234,12 @@ export async function getViewerIptvEpg(viewerId: string, config: Config, streamI
|
|||
const ch = streams.find((s) => s.stream_id === streamId);
|
||||
if (!ch) throw new AppError("NOT_FOUND", "Zender niet gevonden", 404);
|
||||
|
||||
void import("../iptv/active-watches")
|
||||
.then(({ refreshIptvWatch }) =>
|
||||
refreshIptvWatch({ iptvLineId: xt.cacheKey, streamId, title: ch.name })
|
||||
)
|
||||
.catch(() => undefined);
|
||||
|
||||
const cached = getCachedStreamEpg(xt.cacheKey, streamId);
|
||||
if (cached && (cached.now || cached.next)) {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue