diff --git a/package-lock.json b/package-lock.json
index 015d8b2..4395944 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "nodecast-tv",
- "version": "1.5.0",
+ "version": "1.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/public/index.html b/public/index.html
index 01450cc..4b9bca2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -540,6 +540,27 @@
+
+
+ User-Agent
+ HTTP User-Agent for stream requests. Try VLC if your provider blocks
+ Chrome.
+
+
+
+
+
+ Custom User-Agent
+ Enter your own User-Agent string.
+
+
+
diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js
index 25239ae..084dbe6 100644
--- a/public/js/pages/Settings.js
+++ b/public/js/pages/Settings.js
@@ -145,6 +145,39 @@ class SettingsPage {
this.app.player.saveSettings();
});
}
+
+ // User-Agent preset
+ const userAgentSelect = document.getElementById('setting-user-agent');
+ const userAgentCustomInput = document.getElementById('setting-user-agent-custom');
+ const customUaContainer = document.getElementById('custom-user-agent-container');
+
+ if (userAgentSelect && this.app.player?.settings) {
+ // Load saved values
+ userAgentSelect.value = this.app.player.settings.userAgentPreset || 'chrome';
+ if (userAgentCustomInput) {
+ userAgentCustomInput.value = this.app.player.settings.userAgentCustom || '';
+ }
+
+ // Show/hide custom input
+ const toggleCustomInput = () => {
+ if (customUaContainer) {
+ customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
+ }
+ };
+ toggleCustomInput();
+
+ // Save on change
+ userAgentSelect.addEventListener('change', () => {
+ this.app.player.settings.userAgentPreset = userAgentSelect.value;
+ this.app.player.saveSettings();
+ toggleCustomInput();
+ });
+
+ userAgentCustomInput?.addEventListener('change', () => {
+ this.app.player.settings.userAgentCustom = userAgentCustomInput.value;
+ this.app.player.saveSettings();
+ });
+ }
}
initUserManagement() {
@@ -297,6 +330,18 @@ class SettingsPage {
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
+
+ // User-Agent settings
+ const userAgentSelect = document.getElementById('setting-user-agent');
+ const userAgentCustomInput = document.getElementById('setting-user-agent-custom');
+ const customUaContainer = document.getElementById('custom-user-agent-container');
+ if (userAgentSelect) {
+ userAgentSelect.value = s.userAgentPreset || 'chrome';
+ if (customUaContainer) {
+ customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
+ }
+ }
+ if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || '';
}
// Update EPG last refreshed display
diff --git a/server/db.js b/server/db.js
index 9368912..ef38102 100644
--- a/server/db.js
+++ b/server/db.js
@@ -67,10 +67,30 @@ function getDefaultSettings() {
forceRemux: false,
autoTranscode: true,
streamFormat: 'm3u8',
- epgRefreshInterval: '24'
+ epgRefreshInterval: '24',
+ // User-Agent settings
+ userAgentPreset: 'chrome', // chrome | vlc | tivimate | custom
+ userAgentCustom: '', // Custom UA string when preset is 'custom'
+ // Probe cache settings
+ probeCacheTTL: 300, // 5 minutes for URL probe cache
+ seriesProbeCacheDays: 7 // 7 days for series episode probe cache
};
}
+// User-Agent presets
+const USER_AGENT_PRESETS = {
+ chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
+ vlc: 'VLC/3.0.20 LibVLC/3.0.20',
+ tivimate: 'TiviMate/4.7.0',
+};
+
+function getUserAgent(settings) {
+ if (settings.userAgentPreset === 'custom' && settings.userAgentCustom) {
+ return settings.userAgentCustom;
+ }
+ return USER_AGENT_PRESETS[settings.userAgentPreset] || USER_AGENT_PRESETS.chrome;
+}
+
// Write lock to prevent concurrent writes from corrupting db.json
let writeQueue = Promise.resolve();
const tmpPath = dbPath + '.tmp';
@@ -422,4 +442,4 @@ const users = {
}
};
-module.exports = { loadDb, saveDb, sources, hiddenItems, favorites, settings, users, getDefaultSettings };
+module.exports = { loadDb, saveDb, sources, hiddenItems, favorites, settings, users, getDefaultSettings, getUserAgent, USER_AGENT_PRESETS };
diff --git a/server/routes/remux.js b/server/routes/remux.js
index db5fddd..57d4cad 100644
--- a/server/routes/remux.js
+++ b/server/routes/remux.js
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { spawn } = require('child_process');
+const db = require('../db');
/**
* Remux stream (container conversion only)
@@ -12,21 +13,27 @@ const { spawn } = require('child_process');
*
* Note: This does NOT fix Dolby/AC3 audio issues - use /api/transcode for that.
*/
-router.get('/', (req, res) => {
+router.get('/', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL parameter is required' });
}
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
+
+ // Get User-Agent from settings
+ const settings = await db.settings.get();
+ const userAgent = db.getUserAgent(settings);
+
console.log(`[Remux] Starting remux for: ${url}`);
+ console.log(`[Remux] Using User-Agent: ${settings.userAgentPreset}`);
// FFmpeg arguments for pure remux (no encoding)
// Very lightweight - just changes container from TS to fragmented MP4
const args = [
'-hide_banner',
'-loglevel', 'warning',
- '-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
+ '-user_agent', userAgent,
// Low-latency startup: reduce probe/analyze time for faster first bytes
'-probesize', '32768',
'-analyzeduration', '500000', // 0.5 seconds - enough to detect audio
@@ -40,6 +47,8 @@ router.get('/', (req, res) => {
'-reconnect', '1',
'-reconnect_streamed', '1',
'-reconnect_delay_max', '5',
+ // Prevent Range/HEAD requests that some providers reject with 405
+ '-seekable', '0',
'-i', url,
// STRICT MAPPING: Only map video and audio, ignore subtitles/data/attachments
// This prevents remux failure when source container has incompatible subtitle tracks (e.g. MKV -> MP4)
diff --git a/server/routes/transcode.js b/server/routes/transcode.js
index bdc0402..f1e25d9 100644
--- a/server/routes/transcode.js
+++ b/server/routes/transcode.js
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { spawn } = require('child_process');
+const db = require('../db');
/**
* Transcode stream
@@ -9,14 +10,20 @@ const { spawn } = require('child_process');
* Transcodes audio to AAC for browser compatibility while passing video through.
* This fixes playback issues with Dolby/AC3/EAC3 audio that browsers can't decode.
*/
-router.get('/', (req, res) => {
+router.get('/', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'URL parameter is required' });
}
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
+
+ // Get User-Agent from settings
+ const settings = await db.settings.get();
+ const userAgent = db.getUserAgent(settings);
+
console.log(`[Transcode] Starting transcoding for: ${url}`);
+ console.log(`[Transcode] Using User-Agent: ${settings.userAgentPreset}`);
console.log(`[Transcode] Using binary: ${ffmpegPath}`);
// FFmpeg arguments for transcoding
@@ -25,7 +32,7 @@ router.get('/', (req, res) => {
const args = [
'-hide_banner',
'-loglevel', 'warning',
- '-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
+ '-user_agent', userAgent,
// Faster startup - reduced probe/analyze for quicker first bytes
'-probesize', '2000000', // 2MB (reduced from 5MB)
'-analyzeduration', '3000000', // 3 seconds (reduced from 10s)
@@ -39,6 +46,8 @@ router.get('/', (req, res) => {
'-reconnect', '1',
'-reconnect_streamed', '1',
'-reconnect_delay_max', '3',
+ // Prevent Range/HEAD requests that some providers reject with 405
+ '-seekable', '0',
'-i', url,
// Map only first video and audio stream (avoid subtitle streams causing issues)
'-map', '0:v:0',