Add admin button to force-refresh IPTV EPG caches.

Clears global XMLTV and derived caches, re-fetches XUI plus configured feeds, so XUI mapping changes show up without waiting for SWR TTL.
This commit is contained in:
Jos Vooges | STH 2026-09-15 20:49:37 +02:00
parent a7038f04fa
commit 940470a97d
4 changed files with 131 additions and 4 deletions

View file

@ -149,6 +149,7 @@ export default function SettingsPage() {
Array<{ id: string; name: string; channelCount: number }> Array<{ id: string; name: string; channelCount: number }>
>([]); >([]);
const [tvBusy, setTvBusy] = useState(false); const [tvBusy, setTvBusy] = useState(false);
const [tvRefreshBusy, setTvRefreshBusy] = useState(false);
const [tvMsg, setTvMsg] = useState<string | null>(null); const [tvMsg, setTvMsg] = useState<string | null>(null);
const [tvErr, setTvErr] = useState<string | null>(null); const [tvErr, setTvErr] = useState<string | null>(null);
@ -518,6 +519,33 @@ export default function SettingsPage() {
} }
} }
async function refreshTvEpg() {
setTvRefreshBusy(true);
setTvMsg(null);
setTvErr(null);
try {
const res = await fetch("/api/v1/admin/settings/iptv/refresh-epg", {
method: "POST",
credentials: "include",
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setTvErr(data.error?.message ?? "EPG vernieuwen mislukt");
return;
}
const ok = data.xmltvOk ?? 0;
const fail = data.xmltvFailed ?? 0;
setTvMsg(
fail > 0
? `EPG vernieuwd (${ok} ok, ${fail} mislukt) — open TV in de app opnieuw`
: `EPG vernieuwd (${ok} lijn${ok === 1 ? "" : "en"}) — open TV in de app opnieuw`
);
if (Array.isArray(data.categories)) setTvCategories(data.categories);
} finally {
setTvRefreshBusy(false);
}
}
return ( return (
<> <>
<Nav /> <Nav />
@ -876,15 +904,27 @@ export default function SettingsPage() {
))} ))}
</div> </div>
<div className="inline-form" style={{ marginTop: "1rem" }}> <div className="inline-form" style={{ marginTop: "1rem" }}>
<button type="button" disabled={tvBusy} onClick={() => void saveTvSettings()}> <button type="button" disabled={tvBusy || tvRefreshBusy} onClick={() => void saveTvSettings()}>
{tvBusy ? "Bezig…" : "Opslaan"} {tvBusy ? "Bezig…" : "Opslaan"}
</button> </button>
<button type="button" disabled={tvBusy} onClick={() => loadTvSettings()}> <button
Vernieuwen type="button"
disabled={tvBusy || tvRefreshBusy}
onClick={() => void refreshTvEpg()}
title="Leegt EPG-cache en haalt XUI + Admin-URL's opnieuw op"
>
{tvRefreshBusy ? "EPG laden…" : "EPG vernieuwen"}
</button>
<button type="button" disabled={tvBusy || tvRefreshBusy} onClick={() => loadTvSettings()}>
Categorieën herladen
</button> </button>
{tvMsg && <span className="ok-text">{tvMsg}</span>} {tvMsg && <span className="ok-text">{tvMsg}</span>}
{tvErr && <span className="danger-text">{tvErr}</span>} {tvErr && <span className="danger-text">{tvErr}</span>}
</div> </div>
<p className="muted" style={{ marginTop: "0.75rem", fontSize: "0.85rem" }}>
Na een wijziging in XUI: klik <strong>EPG vernieuwen</strong>, daarna in de app de TV-tab
opnieuw openen (of app herstarten) zodat de lijst niet uit lokaal geheugen komt.
</p>
</div> </div>
</section> </section>
)} )}

View file

@ -112,6 +112,28 @@ type GlobalListEntry = { at: number; data: unknown };
const globalLiveListCache = new Map<string, GlobalListEntry>(); const globalLiveListCache = new Map<string, GlobalListEntry>();
const globalLiveListRefreshing = new Set<string>(); const globalLiveListRefreshing = new Set<string>();
/** Leeg alle process-globale XMLTV-caches (na XUI/EPG-wijziging). */
export function invalidateGlobalXmltvCaches() {
globalXmltvGuides.clear();
globalXmltvRefreshing.clear();
}
/** Leeg gecachte live categories/streams. */
export function invalidateGlobalLiveListCaches() {
globalLiveListCache.clear();
globalLiveListRefreshing.clear();
}
export function invalidateAllXtreamGlobalCaches() {
invalidateGlobalXmltvCaches();
invalidateGlobalLiveListCaches();
}
/** Alias voor Admin “EPG vernieuwen”. */
export function invalidateGlobalIptvCaches() {
invalidateAllXtreamGlobalCaches();
}
/** Optionele externe rijke feeds (icons/desc), gezet vanuit admin/config. */ /** Optionele externe rijke feeds (icons/desc), gezet vanuit admin/config. */
let externalEpgXmlUrls: string[] = []; let externalEpgXmlUrls: string[] = [];

View file

@ -11,7 +11,7 @@ import {
upsertGooglePlaySettings, upsertGooglePlaySettings,
} from "./google-play"; } from "./google-play";
import { getIptvSettings, upsertIptvSettings } from "./iptv"; import { getIptvSettings, upsertIptvSettings } from "./iptv";
import { listIptvCategoriesForAdmin } from "../viewer/iptv-client"; import { listIptvCategoriesForAdmin, refreshIptvEpgNow } from "../viewer/iptv-client";
import type { GooglePlayAccessService } from "../google-play/service"; import type { GooglePlayAccessService } from "../google-play/service";
export function registerSettingsRoutes( export function registerSettingsRoutes(
@ -86,4 +86,10 @@ export function registerSettingsRoutes(
const categories = await listIptvCategoriesForAdmin(config); const categories = await listIptvCategoriesForAdmin(config);
return { settings, categories }; return { settings, categories };
}); });
app.post("/api/v1/admin/settings/iptv/refresh-epg", { preHandler: requireAdmin }, async () => {
const result = await refreshIptvEpgNow(config);
const categories = await listIptvCategoriesForAdmin(config);
return { ok: true, ...result, categories };
});
} }

View file

@ -5,6 +5,7 @@ import { loadXtreamByLineId, loadXtreamForViewer } from "../iptv/service";
import { import {
decodeEpgText, decodeEpgText,
epgToIso, epgToIso,
invalidateAllXtreamGlobalCaches,
recomputeGuideNowNext, recomputeGuideNowNext,
type XtreamClient, type XtreamClient,
type XtreamEpgListing, type XtreamEpgListing,
@ -387,6 +388,14 @@ type StreamEpgCacheEntry = {
}; };
const epgStreamCache = new Map<string, StreamEpgCacheEntry>(); const epgStreamCache = new Map<string, StreamEpgCacheEntry>();
/** Wis Nu/Straks + detail-EPG caches (na XUI-wijziging). */
export function invalidateIptvEpgDerivedCaches() {
epgCategoryCache.clear();
epgCategoryRefreshing.clear();
epgStreamCache.clear();
xmltvLineRefreshing.clear();
}
function epgCacheKey(tokenId: string, categoryId?: string): string { function epgCacheKey(tokenId: string, categoryId?: string): string {
return `${tokenId}:${categoryId?.trim() || "__all__"}`; return `${tokenId}:${categoryId?.trim() || "__all__"}`;
} }
@ -819,3 +828,53 @@ export function startIptvEpgWarmup(config: Config) {
setTimeout(() => void tick(), 5_000); setTimeout(() => void tick(), 5_000);
setInterval(() => void tick(), INTERVAL_MS); setInterval(() => void tick(), INTERVAL_MS);
} }
/**
* Admin: caches legen + XMLTV/lijsten opnieuw ophalen zodat XUI-wijzigingen meteen binnenkomen.
*/
export async function refreshIptvEpgNow(config: Config): Promise<{
lines: number;
xmltvOk: number;
xmltvFailed: number;
}> {
invalidateAllXtreamGlobalCaches();
invalidateIptvEpgDerivedCaches();
await applyIptvEpgUrlsToRuntime(config);
const lines = await prisma.iptvLine.findMany({
where: {
enableLive: true,
OR: [
{ viewerUserId: { not: null } },
{ addonToken: { is: { revoked: false } } },
],
},
select: { id: true },
take: 12,
orderBy: { updatedAt: "desc" },
});
let xmltvOk = 0;
let xmltvFailed = 0;
for (const line of lines) {
try {
const xt = await loadXtreamByLineId(line.id, config);
if (!xt?.line.enableLive) continue;
await xt.client.getLiveCategories().catch(() => []);
const all = sortChannels(await xt.client.getLiveStreams().catch(() => []));
await xt.client.getXmltvGuidesByChannelId(0);
if (all.length > 0) {
await resolveCategoryEpgTitles(xt.cacheKey, "__all__", all, xt.client, {
waitForXmltv: true,
});
}
xmltvOk++;
} catch (err) {
xmltvFailed++;
console.warn("[iptv-epg-refresh]", line.id, err);
}
}
return { lines: lines.length, xmltvOk, xmltvFailed };
}