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);
if (!parsed) return [];
// Direct client → XUI URLs. Prefer XUI /play/{token} from m3u playlist
// (classic /live/user/pass/id hangs on real live; works for created_live 24/7).
// Direct client → XUI. Prefer /play/{token} from m3u playlist.
// 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") {
const sid = parsed.streamId;
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) {
out.push({ name: "Live · HLS", title: "XUI play m3u8", url: resolved.hls });
const hint = (filename: string) => ({
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) {
out.push({ name: "Live · MPEG-TS", title: "XUI play ts", url: resolved.ts });
if (resolved.hls && resolved.hls !== 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) {
for (const url of resolved.classic) {
const isTs = url.endsWith(".ts");
out.push({
name: url.endsWith(".ts") ? "Live · MPEG-TS" : "Live · HLS",
name: isTs ? "Live · MPEG-TS" : "Live · HLS",
title: "classic Xtream",
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 s = streams.find((x) => x.stream_id === parsed.streamId);
const ext = s?.container_extension || "mp4";
// VOD/series also non-MP4 often — keep notWebReady for containers != mp4
return [
{
name: `IPTV · ${ext.toUpperCase()}`,
title: s?.name || "VOD",
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}`,
title: ep?.title || `Episode ${parsed.episode}`,
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]);
return {
id: "com.mediacluster.library",
version: "1.5.3",
version: "1.5.4",
name: "Media Cluster",
description: "Privé bibliotheek + Live TV / IPTV (Xtream) met EPG",
catalogs: [...BASE_CATALOGS, ...iptv.catalogs],