Fix IPTV categories timing out under per-category stream fetches.

Return all live categories immediately; only wait briefly for bulk counts and keep a single Alle zenders entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-07 23:21:14 +02:00
parent 318f613b40
commit 1834d42bd5
2 changed files with 16 additions and 36 deletions

View file

@ -205,7 +205,8 @@ export class XtreamClient {
} }
getLiveCategories() { getLiveCategories() {
return this.getJson<XtreamCategory[]>({ action: "get_live_categories" }, 5 * 60_000); // Korte TTL: na IPTV-wijzigingen snel weer alle planken zichtbaar
return this.getJson<XtreamCategory[]>({ action: "get_live_categories" }, 60_000);
} }
getLiveStreams(categoryId?: string) { getLiveStreams(categoryId?: string) {

View file

@ -82,9 +82,14 @@ 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);
// Categorieën zijn leidend — nooit blokkeren op de (soms trage/incomplete) bulk-zenderlijst
const catsRaw = await xt.client.getLiveCategories().catch(() => null); const catsRaw = await xt.client.getLiveCategories().catch(() => null);
const streamsRaw = await xt.client.getLiveStreams().catch(() => null);
const cats = Array.isArray(catsRaw) ? catsRaw : []; const cats = Array.isArray(catsRaw) ? catsRaw : [];
const streamsRaw = await Promise.race([
xt.client.getLiveStreams().catch(() => null),
new Promise<null>((resolve) => setTimeout(() => resolve(null), 6_000)),
]);
const streams = Array.isArray(streamsRaw) ? streamsRaw : []; const streams = Array.isArray(streamsRaw) ? streamsRaw : [];
const counts = new Map<string, number>(); const counts = new Map<string, number>();
@ -94,23 +99,10 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
counts.set(key, (counts.get(key) ?? 0) + 1); counts.set(key, (counts.get(key) ?? 0) + 1);
} }
// Als de bulk-lijst incompleet is: per categorie bijtellen (gecached in XtreamClient)
const idsNeedingCount = cats
.map((c) => normalizeIptvCategoryId(c.category_id))
.filter((id) => id && (counts.get(id) ?? 0) === 0);
if (idsNeedingCount.length > 0) {
await mapPool(idsNeedingCount, 8, async (id) => {
const list = await xt.client.getLiveStreams(id).catch(() => null);
if (Array.isArray(list) && list.length > 0) {
counts.set(id, list.length);
}
});
}
const categories = cats const categories = cats
.map((c) => { .map((c) => {
const id = normalizeIptvCategoryId(c.category_id); const id = normalizeIptvCategoryId(c.category_id);
const rawName = c.category_name ?? ""; const rawName = String(c.category_name ?? "");
return { return {
id, id,
name: friendlyCategoryName(rawName), name: friendlyCategoryName(rawName),
@ -118,26 +110,25 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
channelCount: counts.get(id) ?? 0, channelCount: counts.get(id) ?? 0,
}; };
}) })
// Alleen lege ids + provider-verzamelcategorieën weg (niet echte genre-planken) // Toon ALLE echte planken; alleen pure verzamelbakken van de provider weg
.filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName)) .filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName))
.sort((a, b) => a.name.localeCompare(b.name, "nl")); .sort((a, b) => a.name.localeCompare(b.name, "nl"));
// Één verzamelknop van ons — niet van de IPTV-provider // Één eigen verzamelknop
const totalChannels =
streams.length > 0
? streams.length
: [...counts.values()].reduce((a, b) => a + b, 0);
categories.unshift({ categories.unshift({
id: "__all__", id: "__all__",
name: "Alle zenders", name: "Alle zenders",
rawName: "Alle zenders", rawName: "Alle zenders",
channelCount: totalChannels, channelCount: streams.length,
}); });
return { categories }; return { categories };
} }
/** Alleen echte verzamelbakken (Alles / All / Alle zenders), geen Sport/Films/etc. */ /**
* Alleen pure verzamelbakken (Alles / All), géén "All Sports", "Alle films", enz.
* Exacte match ná friendly-name (landcode-prefix al gestript).
*/
export function isAggregateCategoryName(raw: string): boolean { export function isAggregateCategoryName(raw: string): boolean {
const n = friendlyCategoryName(raw).toLowerCase().replace(/\s+/g, " ").trim(); const n = friendlyCategoryName(raw).toLowerCase().replace(/\s+/g, " ").trim();
return ( return (
@ -162,18 +153,6 @@ export function normalizeIptvCategoryId(raw: unknown): string {
return s; return s;
} }
async function mapPool<T>(items: T[], concurrency: number, fn: (item: T) => Promise<void>) {
const queue = [...items];
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
while (queue.length > 0) {
const item = queue.shift();
if (item === undefined) return;
await fn(item);
}
});
await Promise.all(workers);
}
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));
} }