Fix direct IPTV playback: drop notWebReady and encode Xtream credentials in URLs.
This commit is contained in:
parent
9b7e26041e
commit
b9900e1c3a
3 changed files with 40 additions and 16 deletions
|
|
@ -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 },
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue