From 318f613b40d1e5a4fec82a6981a7714e3cda3a05 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 7 Sep 2026 19:20:31 +0200 Subject: [PATCH] Restore IPTV categories; only drop provider aggregate folders. Keep one Alle zenders entry, backfill per-category counts when the bulk stream list is incomplete, and do not block the linked IPTV line on lastError. Co-authored-by: Cursor --- apps/ios/VMC/UI/LiveTvScreen.swift | 5 +- apps/master-api/src/viewer/iptv-client.ts | 107 ++++++++++++++++------ 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/apps/ios/VMC/UI/LiveTvScreen.swift b/apps/ios/VMC/UI/LiveTvScreen.swift index 8ab81f1..b508d96 100644 --- a/apps/ios/VMC/UI/LiveTvScreen.swift +++ b/apps/ios/VMC/UI/LiveTvScreen.swift @@ -153,10 +153,11 @@ struct LiveTvScreen: View { error = nil do { let raw = try await app.api.loadIptvCategories() - // Geen synthetische "Alles"-chip; API heeft al "Alle zenders" e.d. + // Alleen provider-dubbel "Alles/All" weg; behoud onze "__all__" (Alle zenders) categories = raw.filter { cat in + if cat.id == "__all__" { return true } let n = cat.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - return n != "alles" && n != "all" + return n != "alles" && n != "all" && n != "alle" } let saved = UserDefaults.standard.string(forKey: Self.lastCategoryKey) diff --git a/apps/master-api/src/viewer/iptv-client.ts b/apps/master-api/src/viewer/iptv-client.ts index 3294132..d4e3ab3 100644 --- a/apps/master-api/src/viewer/iptv-client.ts +++ b/apps/master-api/src/viewer/iptv-client.ts @@ -26,19 +26,20 @@ export async function resolveViewerIptvToken(viewerId: string): Promise0: 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) => { - 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; + // 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); } - return a.name.localeCompare(b.name, "nl"); - }); - - if (streams.length > 0) { - categories.unshift({ - id: "__all__", - name: "Alle zenders", - rawName: "Alle zenders", - channelCount: streams.length, }); } + const categories = cats + .map((c) => { + const id = normalizeIptvCategoryId(c.category_id); + const rawName = c.category_name ?? ""; + return { + id, + name: friendlyCategoryName(rawName), + rawName, + channelCount: counts.get(id) ?? 0, + }; + }) + // Alleen lege ids + provider-verzamelcategorieën weg (niet echte genre-planken) + .filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName)) + .sort((a, b) => a.name.localeCompare(b.name, "nl")); + + // Één verzamelknop van ons — niet van de IPTV-provider + const totalChannels = + streams.length > 0 + ? streams.length + : [...counts.values()].reduce((a, b) => a + b, 0); + categories.unshift({ + id: "__all__", + name: "Alle zenders", + rawName: "Alle zenders", + channelCount: totalChannels, + }); + return { categories }; } +/** Alleen echte verzamelbakken (Alles / All / Alle zenders), geen Sport/Films/etc. */ +export function isAggregateCategoryName(raw: string): boolean { + const n = friendlyCategoryName(raw).toLowerCase().replace(/\s+/g, " ").trim(); + return ( + n === "alles" || + n === "all" || + n === "alle" || + n === "alle zenders" || + n === "all channels" || + n === "all channel" || + n === "all categories" || + n === "all cats" || + n === "everything" + ); +} + /** Xtream geeft category_id soms als number, "12" of "12.0". */ export function normalizeIptvCategoryId(raw: unknown): string { if (raw == null) return ""; @@ -125,6 +162,18 @@ export function normalizeIptvCategoryId(raw: unknown): string { return s; } +async function mapPool(items: T[], concurrency: number, fn: (item: T) => Promise) { + 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[] { return [...streams].sort((a, b) => (a.num ?? a.stream_id) - (b.num ?? b.stream_id)); }