Added proxy route for channel icons and other images, updated readme

This commit is contained in:
Trevor Mears
2025-12-29 23:04:31 -08:00
parent 3b14a17bfb
commit 3fffdddf4b
4 changed files with 73 additions and 7 deletions
+5 -3
View File
@@ -157,14 +157,15 @@ If you're running NodeCast TV behind a reverse proxy (Nginx, Caddy, Traefik) wit
| Symptom | Likely Cause | Solution | | Symptom | Likely Cause | Solution |
|---------|--------------|----------| |---------|--------------|----------|
| Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings → Streaming | | 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 | | 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 streams correctly | Ensure your proxy config supports streaming (disable buffering) | | 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:** **Caddy example:**
``` ```
tv.domain.com { tv.domain.com {
reverse_proxy nodecast:3000 { reverse_proxy nodecast:3000 {
flush_interval -1 flush_interval -1
header_up X-Forwarded-Proto {scheme}
} }
} }
``` ```
@@ -176,10 +177,11 @@ location / {
proxy_buffering off; proxy_buffering off;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; 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 ### 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: If you're using TVHeadend as your source, you may need to configure a few settings for streams to play correctly in NodeCast TV:
+16 -3
View File
@@ -27,6 +27,19 @@ class ChannelList {
this.init(); 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 * Load collapsed state from localStorage
*/ */
@@ -399,7 +412,7 @@ class ChannelList {
data-stream-id="${channel.streamId || ''}" data-stream-id="${channel.streamId || ''}"
data-url="${channel.url || ''}" data-url="${channel.url || ''}"
data-render-id="${renderId}"> data-render-id="${renderId}">
<img class="channel-logo" src="${channel.tvgLogo && channel.tvgLogo.length > 0 ? channel.tvgLogo : '/img/placeholder.png'}" <img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'"> alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
<div class="channel-info"> <div class="channel-info">
<div class="channel-name">${this.escapeHtml(channel.name)}</div> <div class="channel-name">${this.escapeHtml(channel.name)}</div>
@@ -851,7 +864,7 @@ class ChannelList {
div.dataset.url = channel.url || ''; div.dataset.url = channel.url || '';
div.innerHTML = ` div.innerHTML = `
<img class="channel-logo" src="${channel.tvgLogo && channel.tvgLogo.length > 0 ? channel.tvgLogo : '/img/placeholder.png'}" <img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'"> alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
<div class="channel-info"> <div class="channel-info">
<div class="channel-name">${this.escapeHtml(channel.name)}</div> <div class="channel-name">${this.escapeHtml(channel.name)}</div>
@@ -1102,7 +1115,7 @@ class ChannelList {
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="epg-info-modal"> <div class="epg-info-modal">
<div class="channel-details"> <div class="channel-details">
<img class="channel-logo" src="${channel.tvgLogo || '/img/placeholder.png'}" <img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
onerror="this.onerror=null;this.src='/img/placeholder.png'" /> onerror="this.onerror=null;this.src='/img/placeholder.png'" />
<div class="channel-meta"> <div class="channel-meta">
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p> <p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
+14 -1
View File
@@ -34,6 +34,19 @@ class EpgGuide {
this.init(); 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() { init() {
this.prevBtn.addEventListener('click', () => this.navigate(-2)); this.prevBtn.addEventListener('click', () => this.navigate(-2));
this.nextBtn.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 // 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); const name = sourceChannel.name || (epgChannel && epgChannel.name);
html += ` html += `
+38
View File
@@ -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; module.exports = router;