Mark IPTV live streams notWebReady and prefer MPEG-TS for XUI playback.

Stremio requires notWebReady for non-MP4 so the local streaming server follows XUI redirects; HLS auth often hangs while TS works.
This commit is contained in:
Jos Vooges | STH 2026-08-25 17:05:51 +02:00
parent 2df8a6a83c
commit 002472e235
2 changed files with 43 additions and 10 deletions

View file

@ -433,27 +433,51 @@ export async function getIptvStreams(
const parsed = parseIptvId(id); const parsed = parseIptvId(id);
if (!parsed) return []; if (!parsed) return [];
// Direct client → XUI URLs. Prefer XUI /play/{token} from m3u playlist // Direct client → XUI. Prefer /play/{token} from m3u playlist.
// (classic /live/user/pass/id hangs on real live; works for created_live 24/7). // Stremio: notWebReady MUST be true for non-MP4 (m3u8/ts) so the local
// streaming server handles redirects — traffic stays client→XUI, not Master.
// Real live HLS often hangs on XUI /auth/*.m3u8; MPEG-TS is reliable.
if (type === "tv" && parsed.kind === "live") { if (type === "tv" && parsed.kind === "live") {
const sid = parsed.streamId; const sid = parsed.streamId;
const resolved = await xt.client.resolveLivePlayUrls(sid); const resolved = await xt.client.resolveLivePlayUrls(sid);
const out: Array<{ name: string; title: string; url: string }> = []; const out: Array<{
name: string;
title: string;
url: string;
behaviorHints: Record<string, unknown>;
}> = [];
if (resolved.hls) { const hint = (filename: string) => ({
out.push({ name: "Live · HLS", title: "XUI play m3u8", url: resolved.hls }); notWebReady: true,
bingeGroup: `mciptv-live-${sid}`,
filename,
});
if (resolved.ts) {
out.push({
name: "Live · MPEG-TS",
title: "XUI · direct ts",
url: resolved.ts,
behaviorHints: hint(`live-${sid}.ts`),
});
} }
if (resolved.ts && resolved.ts !== resolved.hls) { if (resolved.hls && resolved.hls !== resolved.ts) {
out.push({ name: "Live · MPEG-TS", title: "XUI play ts", url: resolved.ts }); out.push({
name: "Live · HLS",
title: "XUI · m3u8 (kan hangen)",
url: resolved.hls,
behaviorHints: hint(`live-${sid}.m3u8`),
});
} }
// Fallback for panels without /play/ playlist (or created_live)
if (out.length === 0) { if (out.length === 0) {
for (const url of resolved.classic) { for (const url of resolved.classic) {
const isTs = url.endsWith(".ts");
out.push({ out.push({
name: url.endsWith(".ts") ? "Live · MPEG-TS" : "Live · HLS", name: isTs ? "Live · MPEG-TS" : "Live · HLS",
title: "classic Xtream", title: "classic Xtream",
url, url,
behaviorHints: hint(isTs ? `live-${sid}.ts` : `live-${sid}.m3u8`),
}); });
} }
} }
@ -465,11 +489,16 @@ export async function getIptvStreams(
const streams = await xt.client.getVodStreams(); const streams = await xt.client.getVodStreams();
const s = streams.find((x) => x.stream_id === parsed.streamId); const s = streams.find((x) => x.stream_id === parsed.streamId);
const ext = s?.container_extension || "mp4"; const ext = s?.container_extension || "mp4";
// VOD/series also non-MP4 often — keep notWebReady for containers != mp4
return [ return [
{ {
name: `IPTV · ${ext.toUpperCase()}`, name: `IPTV · ${ext.toUpperCase()}`,
title: s?.name || "VOD", title: s?.name || "VOD",
url: xt.client.vodStreamUrl(parsed.streamId, ext), url: xt.client.vodStreamUrl(parsed.streamId, ext),
behaviorHints: {
notWebReady: ext.toLowerCase() !== "mp4",
filename: `vod-${parsed.streamId}.${ext}`,
},
}, },
]; ];
} }
@ -489,6 +518,10 @@ export async function getIptvStreams(
name: `IPTV · S${parsed.season}E${parsed.episode}`, name: `IPTV · S${parsed.season}E${parsed.episode}`,
title: ep?.title || `Episode ${parsed.episode}`, title: ep?.title || `Episode ${parsed.episode}`,
url: xt.client.seriesStreamUrl(epId, ext), url: xt.client.seriesStreamUrl(epId, ext),
behaviorHints: {
notWebReady: ext.toLowerCase() !== "mp4",
filename: `series-${parsed.streamId}-s${parsed.season}e${parsed.episode}.${ext}`,
},
}, },
]; ];
} }

View file

@ -41,7 +41,7 @@ async function buildManifest(tokenId: string | null, config: Config) {
const types = new Set(["movie", "series", ...iptv.types]); const types = new Set(["movie", "series", ...iptv.types]);
return { return {
id: "com.mediacluster.library", id: "com.mediacluster.library",
version: "1.5.3", version: "1.5.4",
name: "Media Cluster", name: "Media Cluster",
description: "Privé bibliotheek + Live TV / IPTV (Xtream) met EPG", description: "Privé bibliotheek + Live TV / IPTV (Xtream) met EPG",
catalogs: [...BASE_CATALOGS, ...iptv.catalogs], catalogs: [...BASE_CATALOGS, ...iptv.catalogs],