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);