diff --git a/README.md b/README.md index 21d4976..6fdb93d 100644 --- a/README.md +++ b/README.md @@ -157,14 +157,15 @@ If you're running NodeCast TV behind a reverse proxy (Nginx, Caddy, Traefik) wit | Symptom | Likely Cause | Solution | |---------|--------------|----------| | Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings → Streaming | -| Channel icons don't load | Mixed content on images | Icons are loaded directly; no fix yet for HTTP icons on HTTPS | -| Streams work on HTTP but not HTTPS | Reverse proxy not passing streams correctly | Ensure your proxy config supports streaming (disable buffering) | +| Channel icons don't load | Mixed content on images | Icons are now automatically proxied on HTTPS - update to latest version | +| Streams work on HTTP but not HTTPS | Reverse proxy not passing headers correctly | Ensure `X-Forwarded-Proto` header is set (see examples below) | **Caddy example:** ``` tv.domain.com { reverse_proxy nodecast:3000 { flush_interval -1 + header_up X-Forwarded-Proto {scheme} } } ``` @@ -176,10 +177,11 @@ location / { proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } ``` - ### TVHeadend If you're using TVHeadend as your source, you may need to configure a few settings for streams to play correctly in NodeCast TV: diff --git a/public/js/components/ChannelList.js b/public/js/components/ChannelList.js index 41b3289..6cdac0e 100644 --- a/public/js/components/ChannelList.js +++ b/public/js/components/ChannelList.js @@ -27,6 +27,19 @@ class ChannelList { this.init(); } + /** + * Get proxied image URL to avoid mixed content errors on HTTPS + * Only proxies HTTP URLs when on HTTPS page + */ + getProxiedImageUrl(url) { + if (!url || url.length === 0) return '/img/placeholder.png'; + // Only proxy if we're on HTTPS and the image is HTTP + if (window.location.protocol === 'https:' && url.startsWith('http://')) { + return `/api/proxy/image?url=${encodeURIComponent(url)}`; + } + return url; + } + /** * Load collapsed state from localStorage */ @@ -399,7 +412,7 @@ class ChannelList { data-stream-id="${channel.streamId || ''}" data-url="${channel.url || ''}" data-render-id="${renderId}"> -
${this.escapeHtml(channel.name)}
@@ -851,7 +864,7 @@ class ChannelList { div.dataset.url = channel.url || ''; div.innerHTML = ` -
${this.escapeHtml(channel.name)}
@@ -1102,7 +1115,7 @@ class ChannelList { modalBody.innerHTML = `
-

Group: ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}

diff --git a/public/js/components/EpgGuide.js b/public/js/components/EpgGuide.js index 7970c1f..a5a3907 100644 --- a/public/js/components/EpgGuide.js +++ b/public/js/components/EpgGuide.js @@ -34,6 +34,19 @@ class EpgGuide { this.init(); } + /** + * Get proxied image URL to avoid mixed content errors on HTTPS + * Only proxies HTTP URLs when on HTTPS page + */ + getProxiedImageUrl(url) { + if (!url || url.length === 0) return '/img/placeholder.png'; + // Only proxy if we're on HTTPS and the image is HTTP + if (window.location.protocol === 'https:' && url.startsWith('http://')) { + return `/api/proxy/image?url=${encodeURIComponent(url)}`; + } + return url; + } + init() { this.prevBtn.addEventListener('click', () => this.navigate(-2)); this.nextBtn.addEventListener('click', () => this.navigate(2)); @@ -440,7 +453,7 @@ class EpgGuide { } // Fallback values if EPG channel is missing - const logo = sourceChannel.tvgLogo || (epgChannel && epgChannel.icon) || '/img/placeholder.png'; + const logo = this.getProxiedImageUrl(sourceChannel.tvgLogo || (epgChannel && epgChannel.icon)); const name = sourceChannel.name || (epgChannel && epgChannel.name); html += ` diff --git a/server/routes/proxy.js b/server/routes/proxy.js index e0be1d0..d3e269f 100644 --- a/server/routes/proxy.js +++ b/server/routes/proxy.js @@ -397,4 +397,42 @@ router.get('/stream', async (req, res) => { } }); +/** + * Proxy images (channel logos, posters) + * Fixes mixed content errors when loading HTTP images on HTTPS pages + * GET /api/proxy/image?url=... + */ +router.get('/image', async (req, res) => { + try { + const { url } = req.query; + if (!url) { + return res.status(400).json({ error: 'URL required' }); + } + + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'image/*,*/*;q=0.8' + } + }); + + if (!response.ok) { + return res.status(response.status).send('Failed to fetch image'); + } + + // Forward content type + const contentType = response.headers.get('content-type') || 'image/png'; + res.set('Content-Type', contentType); + res.set('Access-Control-Allow-Origin', '*'); + res.set('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours + + // Pipe the image data + const buffer = await response.arrayBuffer(); + res.send(Buffer.from(buffer)); + } catch (err) { + console.error('Image proxy error:', err.message); + res.status(500).send('Image proxy error'); + } +}); + module.exports = router;