From 6a0eee0313f1e900b7503646ab02b377b1ddc591 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 7 Sep 2026 19:14:12 +0200 Subject: [PATCH] Fix missing IPTV categories hidden by zero channelCount filter. Show all live categories even when the full stream list is incomplete or category ids do not match exactly; harden Xtream list parsing. Co-authored-by: Cursor --- apps/master-api/src/iptv/xtream.ts | 17 ++++++++- apps/master-api/src/viewer/iptv-client.ts | 46 +++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/apps/master-api/src/iptv/xtream.ts b/apps/master-api/src/iptv/xtream.ts index e3f46ee..91bb554 100644 --- a/apps/master-api/src/iptv/xtream.ts +++ b/apps/master-api/src/iptv/xtream.ts @@ -150,12 +150,27 @@ export class XtreamClient { const res = await fetch(url, { headers: { "User-Agent": "MediaCluster/1.0" }, - signal: AbortSignal.timeout(20000), + // Grote live-lijsten kunnen langer duren dan 20s + signal: AbortSignal.timeout( + params.action === "get_live_streams" && !params.category_id ? 60_000 : 25_000 + ), }); if (!res.ok) { throw new Error(`Xtream API HTTP ${res.status}`); } const data = (await res.json()) as T; + // Nooit een niet-array cachen voor list-actions (Xtream geeft soms auth/error-object) + if ( + (params.action === "get_live_categories" || + params.action === "get_live_streams" || + params.action === "get_vod_categories" || + params.action === "get_vod_streams" || + params.action === "get_series_categories" || + params.action === "get_series") && + !Array.isArray(data) + ) { + throw new Error(`Xtream API gaf geen lijst terug voor ${params.action}`); + } this.cacheSet(cacheKey, data); return data; } diff --git a/apps/master-api/src/viewer/iptv-client.ts b/apps/master-api/src/viewer/iptv-client.ts index 5b9b6e1..3294132 100644 --- a/apps/master-api/src/viewer/iptv-client.ts +++ b/apps/master-api/src/viewer/iptv-client.ts @@ -71,24 +71,38 @@ export async function getViewerIptvCategories(viewerId: string, config: Config) const xt = await loadXtreamForToken(tokenId, config); if (!xt?.line.enableLive) throw new AppError("FORBIDDEN", "Live TV niet beschikbaar", 403); - const cats = await xt.client.getLiveCategories().catch(() => []); - const streams = await xt.client.getLiveStreams().catch(() => []); + const catsRaw = await xt.client.getLiveCategories().catch(() => null); + const streamsRaw = await xt.client.getLiveStreams().catch(() => null); + const cats = Array.isArray(catsRaw) ? catsRaw : []; + const streams = Array.isArray(streamsRaw) ? streamsRaw : []; const counts = new Map(); for (const s of streams) { - const key = String(s.category_id ?? ""); + const key = normalizeIptvCategoryId(s.category_id); + if (!key) continue; counts.set(key, (counts.get(key) ?? 0) + 1); } + // Geen filter op channelCount>0: de volledige get_live_streams-lijst is soms + // incompleet of category_id-format wijkt af, waardoor echte categorieën verdwenen. const categories = cats - .map((c) => ({ - id: String(c.category_id), - name: friendlyCategoryName(c.category_name), - rawName: c.category_name, - channelCount: counts.get(String(c.category_id)) ?? 0, - })) - .filter((c) => c.channelCount > 0) - .sort((a, b) => a.name.localeCompare(b.name, "nl")); + .map((c) => { + const id = normalizeIptvCategoryId(c.category_id); + return { + id, + name: friendlyCategoryName(c.category_name), + rawName: c.category_name, + channelCount: counts.get(id) ?? 0, + }; + }) + .filter((c) => c.id.length > 0) + .sort((a, b) => { + // Categorieën mét getelde zenders eerst, daarna alfabetisch + if ((a.channelCount > 0) !== (b.channelCount > 0)) { + return a.channelCount > 0 ? -1 : 1; + } + return a.name.localeCompare(b.name, "nl"); + }); if (streams.length > 0) { categories.unshift({ @@ -102,6 +116,15 @@ export async function getViewerIptvCategories(viewerId: string, config: Config) return { categories }; } +/** Xtream geeft category_id soms als number, "12" of "12.0". */ +export function normalizeIptvCategoryId(raw: unknown): string { + if (raw == null) return ""; + const s = String(raw).trim(); + if (!s) return ""; + if (/^\d+(\.0+)?$/.test(s)) return String(parseInt(s, 10)); + return s; +} + function sortChannels(streams: XtreamLiveStream[]): XtreamLiveStream[] { return [...streams].sort((a, b) => (a.num ?? a.stream_id) - (b.num ?? b.stream_id)); } @@ -124,6 +147,7 @@ export async function getViewerIptvChannels( !categoryId || categoryId === "__all__" ? await xt.client.getLiveStreams() : await xt.client.getLiveStreams(categoryId); + if (!Array.isArray(streams)) streams = []; streams = sortChannels(streams); const epgTitles = includeEpg