Include custom live lists in admin TV category order.

Lists appear alongside XUI categories and respect the saved order in apps.
This commit is contained in:
Jos Vooges | STH 2026-09-18 20:08:00 +02:00
parent c97374c9cb
commit 94bf98ded7
2 changed files with 84 additions and 59 deletions

View file

@ -919,9 +919,10 @@ export default function SettingsPage() {
Categorie-volgorde Categorie-volgorde
</h3> </h3>
<p className="muted" style={{ marginBottom: "0.75rem" }}> <p className="muted" style={{ marginBottom: "0.75rem" }}>
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 {tvCategories.length === 0
? " Geen actieve IPTV-lijn gevonden — koppel eerst een kijker." ? " Geen categorieën — koppel een IPTV-lijn en/of maak Live-lijsten."
: ""} : ""}
</p> </p>
<div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>

View file

@ -84,24 +84,27 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
throw new AppError("FORBIDDEN", "Geen TV-toegang", 403); throw new AppError("FORBIDDEN", "Geen TV-toegang", 403);
} }
const prefs = await getIptvSettings(config);
let categories: Array<{ let categories: Array<{
id: string; id: string;
name: string; name: string;
rawName: string; rawName: string;
channelCount: number; channelCount: number;
}> = []; }> = [];
let streamCountForAll = 0;
if (xt?.line.enableLive) { if (xt?.line.enableLive) {
const [catsRaw, streamsRaw, iptvPrefs] = await Promise.all([ const [catsRaw, streamsRaw] = await Promise.all([
xt.client.getLiveCategories().catch(() => [] as Awaited<ReturnType<typeof xt.client.getLiveCategories>>), xt.client.getLiveCategories().catch(() => [] as Awaited<ReturnType<typeof xt.client.getLiveCategories>>),
Promise.race([ Promise.race([
xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]), xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]),
new Promise<XtreamLiveStream[]>((resolve) => setTimeout(() => resolve([]), 6_000)), new Promise<XtreamLiveStream[]>((resolve) => setTimeout(() => resolve([]), 6_000)),
]), ]),
getIptvSettings(config),
]); ]);
const cats = Array.isArray(catsRaw) ? catsRaw : []; const cats = Array.isArray(catsRaw) ? catsRaw : [];
const streams: XtreamLiveStream[] = Array.isArray(streamsRaw) ? streamsRaw : []; const streams: XtreamLiveStream[] = Array.isArray(streamsRaw) ? streamsRaw : [];
streamCountForAll = streams.length;
const counts = new Map<string, number>(); const counts = new Map<string, number>();
for (const s of streams) { for (const s of streams) {
@ -131,18 +134,6 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
channelCount, 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); const customLists = await listAssignedLiveListsForViewer(viewerId);
@ -155,30 +146,46 @@ 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 }; 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) { 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({ const line = await prisma.iptvLine.findFirst({
where: { enableLive: true }, where: { enableLive: true },
orderBy: { updatedAt: "desc" }, orderBy: { updatedAt: "desc" },
select: { id: true }, select: { id: true },
}); });
if (!line) return [] as Array<{ id: string; name: string; channelCount: number }>;
if (line) {
const xt = await loadXtreamByLineId(line.id, config); const xt = await loadXtreamByLineId(line.id, config);
if (!xt?.line.enableLive) { if (xt?.line.enableLive) {
return [] as Array<{ id: string; name: string; channelCount: number }>; const [catsRaw, streamsRaw] = await Promise.all([
}
const [catsRaw, streamsRaw, iptvPrefs] = await Promise.all([
xt.client.getLiveCategories().catch(() => []), xt.client.getLiveCategories().catch(() => []),
Promise.race([ Promise.race([
xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]), xt.client.getLiveStreams().catch(() => [] as XtreamLiveStream[]),
new Promise<XtreamLiveStream[]>((resolve) => setTimeout(() => resolve([]), 8_000)), new Promise<XtreamLiveStream[]>((resolve) => setTimeout(() => resolve([]), 8_000)),
]), ]),
getIptvSettings(config),
]); ]);
const cats = Array.isArray(catsRaw) ? catsRaw : []; const cats = Array.isArray(catsRaw) ? catsRaw : [];
const streams = Array.isArray(streamsRaw) ? streamsRaw : []; const streams = Array.isArray(streamsRaw) ? streamsRaw : [];
@ -190,7 +197,7 @@ export async function listIptvCategoriesForAdmin(config: Config) {
counts.set(key, (counts.get(key) ?? 0) + 1); counts.set(key, (counts.get(key) ?? 0) + 1);
} }
let categories = cats categories = cats
.map((c) => { .map((c) => {
const id = normalizeIptvCategoryId(c.category_id); const id = normalizeIptvCategoryId(c.category_id);
const rawName = String(c.category_name ?? ""); const rawName = String(c.category_name ?? "");
@ -211,6 +218,23 @@ export async function listIptvCategoriesForAdmin(config: Config) {
channelCount, 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); categories = applyIptvCategoryOrder(categories, iptvPrefs.categoryOrder);
return categories.map(({ id, name, channelCount }) => ({ id, name, channelCount })); return categories.map(({ id, name, channelCount }) => ({ id, name, channelCount }));