diff --git a/apps/admin-ui/src/app/settings/page.tsx b/apps/admin-ui/src/app/settings/page.tsx index cb41d72..7c3a07e 100644 --- a/apps/admin-ui/src/app/settings/page.tsx +++ b/apps/admin-ui/src/app/settings/page.tsx @@ -919,9 +919,10 @@ export default function SettingsPage() { Categorie-volgorde

- Volgorde van links naar rechts in de TV-apps. Nieuwe XUI-categorieën komen onderaan. + Volgorde van links naar rechts in de TV-apps. XUI-categorieën én Live-lijsten + (ClearKey/DASH). Nieuwe items komen onderaan. {tvCategories.length === 0 - ? " Geen actieve IPTV-lijn gevonden — koppel eerst een kijker." + ? " Geen categorieën — koppel een IPTV-lijn en/of maak Live-lijsten." : ""}

diff --git a/apps/master-api/src/viewer/iptv-client.ts b/apps/master-api/src/viewer/iptv-client.ts index e07fe0b..e74ad3c 100644 --- a/apps/master-api/src/viewer/iptv-client.ts +++ b/apps/master-api/src/viewer/iptv-client.ts @@ -84,24 +84,27 @@ export async function getViewerIptvCategories(viewerId: string, config: Config) throw new AppError("FORBIDDEN", "Geen TV-toegang", 403); } + const prefs = await getIptvSettings(config); + let categories: Array<{ id: string; name: string; rawName: string; channelCount: number; }> = []; + let streamCountForAll = 0; if (xt?.line.enableLive) { - const [catsRaw, streamsRaw, iptvPrefs] = await Promise.all([ + const [catsRaw, streamsRaw] = await Promise.all([ xt.client.getLiveCategories().catch(() => [] as Awaited>), Promise.race([ xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]), new Promise((resolve) => setTimeout(() => resolve([]), 6_000)), ]), - getIptvSettings(config), ]); const cats = Array.isArray(catsRaw) ? catsRaw : []; const streams: XtreamLiveStream[] = Array.isArray(streamsRaw) ? streamsRaw : []; + streamCountForAll = streams.length; const counts = new Map(); for (const s of streams) { @@ -131,18 +134,6 @@ export async function getViewerIptvCategories(viewerId: string, config: Config) channelCount, })); } - - const prefs = iptvPrefs ?? (await getIptvSettings(config)); - categories = applyIptvCategoryOrder(categories, prefs.categoryOrder); - - if (prefs.showAlleZenders) { - categories.unshift({ - id: "__all__", - name: "Alle zenders", - rawName: "Alle zenders", - channelCount: streams.length, - }); - } } const customLists = await listAssignedLiveListsForViewer(viewerId); @@ -155,61 +146,94 @@ export async function getViewerIptvCategories(viewerId: string, config: Config) }); } + categories = applyIptvCategoryOrder(categories, prefs.categoryOrder); + + if (prefs.showAlleZenders && xt?.line.enableLive) { + categories.unshift({ + id: "__all__", + name: "Alle zenders", + rawName: "Alle zenders", + channelCount: streamCountForAll, + }); + } + return { categories }; } -/** Admin: categorieën uit een actieve IPTV-lijn voor volgorde-UI. */ +/** Admin: XUI-categorieën + custom live-lijsten voor volgorde-UI. */ export async function listIptvCategoriesForAdmin(config: Config) { + const iptvPrefs = await getIptvSettings(config); + + let categories: Array<{ + id: string; + name: string; + rawName: string; + channelCount: number; + }> = []; + const line = await prisma.iptvLine.findFirst({ where: { enableLive: true }, orderBy: { updatedAt: "desc" }, select: { id: true }, }); - if (!line) return [] as Array<{ id: string; name: string; channelCount: number }>; - const xt = await loadXtreamByLineId(line.id, config); - if (!xt?.line.enableLive) { - return [] as Array<{ id: string; name: string; channelCount: number }>; + if (line) { + const xt = await loadXtreamByLineId(line.id, config); + if (xt?.line.enableLive) { + const [catsRaw, streamsRaw] = await Promise.all([ + xt.client.getLiveCategories().catch(() => []), + Promise.race([ + xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]), + new Promise((resolve) => setTimeout(() => resolve([]), 8_000)), + ]), + ]); + const cats = Array.isArray(catsRaw) ? catsRaw : []; + const streams = Array.isArray(streamsRaw) ? streamsRaw : []; + + const counts = new Map(); + for (const s of streams) { + const key = normalizeIptvCategoryId(s.category_id); + if (!key) continue; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + + categories = cats + .map((c) => { + const id = normalizeIptvCategoryId(c.category_id); + const rawName = String(c.category_name ?? ""); + return { + id, + name: friendlyCategoryName(rawName), + rawName, + channelCount: counts.get(id) ?? 0, + }; + }) + .filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName)); + + if (categories.length === 0 && counts.size > 0) { + categories = [...counts.entries()].map(([id, channelCount]) => ({ + id, + name: `Groep ${id}`, + rawName: `Groep ${id}`, + channelCount, + })); + } + } } - const [catsRaw, streamsRaw, iptvPrefs] = await Promise.all([ - xt.client.getLiveCategories().catch(() => []), - Promise.race([ - xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]), - new Promise((resolve) => setTimeout(() => resolve([]), 8_000)), - ]), - getIptvSettings(config), - ]); - const cats = Array.isArray(catsRaw) ? catsRaw : []; - const streams = Array.isArray(streamsRaw) ? streamsRaw : []; - - const counts = new Map(); - for (const s of streams) { - const key = normalizeIptvCategoryId(s.category_id); - if (!key) continue; - counts.set(key, (counts.get(key) ?? 0) + 1); - } - - let categories = cats - .map((c) => { - const id = normalizeIptvCategoryId(c.category_id); - const rawName = String(c.category_name ?? ""); - return { - id, - name: friendlyCategoryName(rawName), - rawName, - channelCount: counts.get(id) ?? 0, - }; - }) - .filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName)); - - if (categories.length === 0 && counts.size > 0) { - categories = [...counts.entries()].map(([id, channelCount]) => ({ - id, - name: `Groep ${id}`, - rawName: `Groep ${id}`, - channelCount, - })); + const customLists = await prisma.liveChannelList.findMany({ + where: { enabled: true }, + orderBy: [{ sortOrder: "asc" }, { name: "asc" }], + include: { _count: { select: { channels: { where: { enabled: true } } } } }, + }); + const { customCategoryId } = await import("./live-channels"); + for (const list of customLists) { + categories.push({ + id: customCategoryId(list.id), + name: list.name, + rawName: list.name, + channelCount: list._count.channels, + }); } categories = applyIptvCategoryOrder(categories, iptvPrefs.categoryOrder);