222 lines
6.1 KiB
TypeScript
222 lines
6.1 KiB
TypeScript
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,
|
|
});
|
|
}
|