From 0c15d6129196561b81be21e9d9493f514f2e4ba2 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 15 Sep 2026 18:33:50 +0200 Subject: [PATCH] Add admin TV settings for EPG URLs, category order, and Alle zenders. Store cluster-wide IPTV UI prefs in IntegrationSetting and apply them to client category lists and multi-feed XMLTV merge. --- apps/admin-ui/src/app/settings/page.tsx | 185 +++++++++++++++++++- apps/master-api/.env.example | 3 +- apps/master-api/src/app.ts | 4 +- apps/master-api/src/iptv/xtream.ts | 55 ++++-- apps/master-api/src/settings/iptv.ts | 196 ++++++++++++++++++++++ apps/master-api/src/settings/routes.ts | 21 +++ apps/master-api/src/viewer/iptv-client.ts | 109 +++++++++--- 7 files changed, 528 insertions(+), 45 deletions(-) create mode 100644 apps/master-api/src/settings/iptv.ts diff --git a/apps/admin-ui/src/app/settings/page.tsx b/apps/admin-ui/src/app/settings/page.tsx index d3dce29..d265380 100644 --- a/apps/admin-ui/src/app/settings/page.tsx +++ b/apps/admin-ui/src/app/settings/page.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { Nav, useAuth } from "@/components/Nav"; -type SettingsTab = "admin" | "shelves" | "download-station" | "opensubtitles" | "google-play"; +type SettingsTab = "admin" | "shelves" | "download-station" | "opensubtitles" | "google-play" | "tv"; function parseTab(v: string | null): SettingsTab { if ( @@ -12,7 +12,8 @@ function parseTab(v: string | null): SettingsTab { v === "download-station" || v === "opensubtitles" || v === "shelves" || - v === "google-play" + v === "google-play" || + v === "tv" ) { return v; } @@ -57,6 +58,7 @@ interface DsConfig { const TABS: Array<{ id: SettingsTab; label: string; short: string }> = [ { id: "admin", label: "Admin", short: "Admin" }, { id: "shelves", label: "Planken", short: "Planken" }, + { id: "tv", label: "TV", short: "TV" }, { id: "download-station", label: "Download Station", short: "DS" }, { id: "opensubtitles", label: "OpenSubtitles", short: "Subs" }, { id: "google-play", label: "Google Play", short: "Play" }, @@ -141,6 +143,15 @@ export default function SettingsPage() { const [gpMsg, setGpMsg] = useState(null); const [gpErr, setGpErr] = useState(null); + const [tvEpgUrlsText, setTvEpgUrlsText] = useState(""); + const [tvShowAlle, setTvShowAlle] = useState(true); + const [tvCategories, setTvCategories] = useState< + Array<{ id: string; name: string; channelCount: number }> + >([]); + const [tvBusy, setTvBusy] = useState(false); + const [tvMsg, setTvMsg] = useState(null); + const [tvErr, setTvErr] = useState(null); + const loadShelves = useCallback(() => { fetch("/api/v1/admin/library/shelves", { credentials: "include" }) .then((r) => r.json()) @@ -204,11 +215,26 @@ export default function SettingsPage() { .catch(() => undefined); }, []); + const loadTvSettings = useCallback(() => { + fetch("/api/v1/admin/settings/iptv", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => { + const s = d.settings; + if (s) { + setTvEpgUrlsText(Array.isArray(s.epgXmlUrls) ? s.epgXmlUrls.join("\n") : ""); + setTvShowAlle(s.showAlleZenders !== false); + } + setTvCategories(Array.isArray(d.categories) ? d.categories : []); + }) + .catch(() => undefined); + }, []); + useEffect(() => { loadShelves(); loadDs(); loadOpenSubtitles(); loadGooglePlay(); + loadTvSettings(); fetch("/api/v1/auth/me", { credentials: "include" }) .then((r) => r.json()) .then((d) => setAdminEmail(d.user?.email ?? "")) @@ -221,7 +247,7 @@ export default function SettingsPage() { } }) .catch(() => undefined); - }, [loadShelves, loadDs, loadOpenSubtitles, loadGooglePlay]); + }, [loadShelves, loadDs, loadOpenSubtitles, loadGooglePlay, loadTvSettings]); async function saveInstallPageEnabled(enabled: boolean) { setInstallPageBusy(true); @@ -443,6 +469,55 @@ export default function SettingsPage() { } } + function moveTvCategory(index: number, dir: -1 | 1) { + setTvCategories((prev) => { + const next = [...prev]; + const j = index + dir; + if (j < 0 || j >= next.length) return prev; + const tmp = next[index]; + next[index] = next[j]; + next[j] = tmp; + return next; + }); + } + + async function saveTvSettings() { + setTvBusy(true); + setTvMsg(null); + setTvErr(null); + try { + const epgXmlUrls = tvEpgUrlsText + .split(/[\n,]+/) + .map((u) => u.trim()) + .filter(Boolean); + const res = await fetch("/api/v1/admin/settings/iptv", { + method: "PUT", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + epgXmlUrls, + categoryOrder: tvCategories.map((c) => c.id), + showAlleZenders: tvShowAlle, + }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setTvErr(data.error?.message ?? "Opslaan mislukt"); + return; + } + setTvMsg("Opgeslagen — geldt voor alle accounts"); + if (data.settings) { + setTvEpgUrlsText( + Array.isArray(data.settings.epgXmlUrls) ? data.settings.epgXmlUrls.join("\n") : "" + ); + setTvShowAlle(data.settings.showAlleZenders !== false); + } + if (Array.isArray(data.categories)) setTvCategories(data.categories); + } finally { + setTvBusy(false); + } + } + return ( <>