diff --git a/apps/master-api/src/iptv/service.ts b/apps/master-api/src/iptv/service.ts index aa57ee6..44ffcc4 100644 --- a/apps/master-api/src/iptv/service.ts +++ b/apps/master-api/src/iptv/service.ts @@ -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 }, }, ]; } diff --git a/apps/master-api/src/iptv/xtream.ts b/apps/master-api/src/iptv/xtream.ts index a61f127..deafdf9 100644 --- a/apps/master-api/src/iptv/xtream.ts +++ b/apps/master-api/src/iptv/xtream.ts @@ -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}`; } } diff --git a/apps/master-api/src/stremio/routes.ts b/apps/master-api/src/stremio/routes.ts index fa4eaa8..f157cb4 100644 --- a/apps/master-api/src/stremio/routes.ts +++ b/apps/master-api/src/stremio/routes.ts @@ -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 }; }