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 <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-07 19:14:12 +02:00
parent cf9cdd43db
commit 6a0eee0313
2 changed files with 51 additions and 12 deletions

View file

@ -150,12 +150,27 @@ export class XtreamClient {
const res = await fetch(url, { const res = await fetch(url, {
headers: { "User-Agent": "MediaCluster/1.0" }, 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) { if (!res.ok) {
throw new Error(`Xtream API HTTP ${res.status}`); throw new Error(`Xtream API HTTP ${res.status}`);
} }
const data = (await res.json()) as T; 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); this.cacheSet(cacheKey, data);
return data; return data;
} }

View file

@ -71,24 +71,38 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
const xt = await loadXtreamForToken(tokenId, config); const xt = await loadXtreamForToken(tokenId, config);
if (!xt?.line.enableLive) throw new AppError("FORBIDDEN", "Live TV niet beschikbaar", 403); if (!xt?.line.enableLive) throw new AppError("FORBIDDEN", "Live TV niet beschikbaar", 403);
const cats = await xt.client.getLiveCategories().catch(() => []); const catsRaw = await xt.client.getLiveCategories().catch(() => null);
const streams = await xt.client.getLiveStreams().catch(() => []); const streamsRaw = await xt.client.getLiveStreams().catch(() => null);
const cats = Array.isArray(catsRaw) ? catsRaw : [];
const streams = Array.isArray(streamsRaw) ? streamsRaw : [];
const counts = new Map<string, number>(); const counts = new Map<string, number>();
for (const s of streams) { 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); 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 const categories = cats
.map((c) => ({ .map((c) => {
id: String(c.category_id), const id = normalizeIptvCategoryId(c.category_id);
name: friendlyCategoryName(c.category_name), return {
rawName: c.category_name, id,
channelCount: counts.get(String(c.category_id)) ?? 0, name: friendlyCategoryName(c.category_name),
})) rawName: c.category_name,
.filter((c) => c.channelCount > 0) channelCount: counts.get(id) ?? 0,
.sort((a, b) => a.name.localeCompare(b.name, "nl")); };
})
.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) { if (streams.length > 0) {
categories.unshift({ categories.unshift({
@ -102,6 +116,15 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
return { categories }; 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[] { function sortChannels(streams: XtreamLiveStream[]): XtreamLiveStream[] {
return [...streams].sort((a, b) => (a.num ?? a.stream_id) - (b.num ?? b.stream_id)); 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__" !categoryId || categoryId === "__all__"
? await xt.client.getLiveStreams() ? await xt.client.getLiveStreams()
: await xt.client.getLiveStreams(categoryId); : await xt.client.getLiveStreams(categoryId);
if (!Array.isArray(streams)) streams = [];
streams = sortChannels(streams); streams = sortChannels(streams);
const epgTitles = includeEpg const epgTitles = includeEpg