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);
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") {
const urlM3u8 = xt.client.liveStreamUrl(parsed.streamId, "m3u8");
const urlTs = xt.client.liveStreamUrl(parsed.streamId, "ts");
const sid = parsed.streamId;
return [
{
name: "Live · HLS",
title: "Xtream m3u8",
url: urlM3u8,
behaviorHints: { notWebReady: true },
title: "m3u8",
url: xt.client.liveStreamUrl(sid, "m3u8"),
},
{
name: "Live · TS",
title: "Xtream mpegts",
url: urlTs,
behaviorHints: { notWebReady: true },
name: "Live · MPEG-TS",
title: "ts",
url: xt.client.liveStreamUrl(sid, "ts"),
},
{
name: "Live · raw",
title: "no extension",
url: xt.client.liveStreamUrl(sid, ""),
},
];
}
@ -461,7 +465,6 @@ export async function getIptvStreams(
name: `IPTV · ${ext.toUpperCase()}`,
title: s?.name || "VOD",
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}`,
title: ep?.title || `Episode ${parsed.episode}`,
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 {
return `${this.baseUrl}/live/${this.username}/${this.password}/${streamId}.${ext}`;
liveStreamUrl(streamId: number, ext: "m3u8" | "ts" | "" = "m3u8"): string {
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 {
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 {
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]);
return {
id: "com.mediacluster.library",
version: "1.5.1",
version: "1.5.2",
name: "Media Cluster",
description: "Privé bibliotheek + Live TV / IPTV (Xtream) met EPG",
catalogs: [...BASE_CATALOGS, ...iptv.catalogs],
@ -95,6 +95,17 @@ function paramId(raw: string): string {
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.
* 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") {
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 };
}