Fix direct IPTV playback: drop notWebReady and encode Xtream credentials in URLs.

This commit is contained in:
Jos Vooges | STH 2026-08-25 16:42:14 +02:00
parent 9b7e26041e
commit b9900e1c3a
3 changed files with 40 additions and 16 deletions

View file

@ -433,21 +433,25 @@ export async function getIptvStreams(
const parsed = parseIptvId(id); const parsed = parseIptvId(id);
if (!parsed) return []; if (!parsed) return [];
// Direct Xtream/XUI URLs only (client → IPTV server). No Master proxy.
// Do NOT set notWebReady — that skips playback and XUI never sees a connection.
if (type === "tv" && parsed.kind === "live") { if (type === "tv" && parsed.kind === "live") {
const urlM3u8 = xt.client.liveStreamUrl(parsed.streamId, "m3u8"); const sid = parsed.streamId;
const urlTs = xt.client.liveStreamUrl(parsed.streamId, "ts");
return [ return [
{ {
name: "Live · HLS", name: "Live · HLS",
title: "Xtream m3u8", title: "m3u8",
url: urlM3u8, url: xt.client.liveStreamUrl(sid, "m3u8"),
behaviorHints: { notWebReady: true },
}, },
{ {
name: "Live · TS", name: "Live · MPEG-TS",
title: "Xtream mpegts", title: "ts",
url: urlTs, url: xt.client.liveStreamUrl(sid, "ts"),
behaviorHints: { notWebReady: true }, },
{
name: "Live · raw",
title: "no extension",
url: xt.client.liveStreamUrl(sid, ""),
}, },
]; ];
} }
@ -461,7 +465,6 @@ export async function getIptvStreams(
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: true },
}, },
]; ];
} }
@ -481,7 +484,6 @@ 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: true },
}, },
]; ];
} }

View file

@ -253,16 +253,23 @@ export class XtreamClient {
} }
} }
liveStreamUrl(streamId: number, ext: "m3u8" | "ts" = "m3u8"): string { liveStreamUrl(streamId: number, ext: "m3u8" | "ts" | "" = "m3u8"): string {
return `${this.baseUrl}/live/${this.username}/${this.password}/${streamId}.${ext}`; const user = encodeURIComponent(this.username);
const pass = encodeURIComponent(this.password);
if (!ext) return `${this.baseUrl}/live/${user}/${pass}/${streamId}`;
return `${this.baseUrl}/live/${user}/${pass}/${streamId}.${ext}`;
} }
vodStreamUrl(streamId: number, ext = "mp4"): string { vodStreamUrl(streamId: number, ext = "mp4"): string {
return `${this.baseUrl}/movie/${this.username}/${this.password}/${streamId}.${ext}`; const user = encodeURIComponent(this.username);
const pass = encodeURIComponent(this.password);
return `${this.baseUrl}/movie/${user}/${pass}/${streamId}.${ext}`;
} }
seriesStreamUrl(episodeId: string, ext = "mp4"): string { seriesStreamUrl(episodeId: string, ext = "mp4"): string {
return `${this.baseUrl}/series/${this.username}/${this.password}/${episodeId}.${ext}`; const user = encodeURIComponent(this.username);
const pass = encodeURIComponent(this.password);
return `${this.baseUrl}/series/${user}/${pass}/${episodeId}.${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.1", version: "1.5.2",
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],
@ -95,6 +95,17 @@ function paramId(raw: string): string {
return decodeURIComponent(raw).replace(/\.json$/i, ""); return decodeURIComponent(raw).replace(/\.json$/i, "");
} }
/** Log-safe host from a stream URL (no credentials). */
function safeStreamHost(url?: string): string | undefined {
if (!url) return undefined;
try {
const u = new URL(url);
return `${u.protocol}//${u.host}`;
} catch {
return "[invalid-url]";
}
}
/** /**
* Stremio requires Video.released (ISO-8601); missing/invalid videos are dropped. * Stremio requires Video.released (ISO-8601); missing/invalid videos are dropped.
* Always unique per S/E — identical `released` values make some clients collapse the list. * Always unique per S/E — identical `released` values make some clients collapse the list.
@ -227,6 +238,10 @@ export async function registerStremioRoutes(app: FastifyInstance, config: Config
if (id.startsWith(IPTV_PREFIX) || type === "tv") { if (id.startsWith(IPTV_PREFIX) || type === "tv") {
const streams = await getIptvStreams(auth.tokenId, config, type, id); const streams = await getIptvStreams(auth.tokenId, config, type, id);
request.log.info(
{ type, id, streamCount: streams.length, sampleHost: safeStreamHost(streams[0]?.url) },
"iptv streams resolved (direct to Xtream)"
);
return { streams }; return { streams };
} }