feat: add -seekable 0 to fix 405 errors and configurable User-Agent presets for stream requests

This commit is contained in:
Trevor Mears
2026-01-17 00:45:55 -08:00
parent c1a1ac4faa
commit 2a03cad213
6 changed files with 111 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "nodecast-tv", "name": "nodecast-tv",
"version": "1.5.0", "version": "1.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
+21
View File
@@ -540,6 +540,27 @@
<option value="ts">MPEG-TS (ts) - Better buffer</option> <option value="ts">MPEG-TS (ts) - Better buffer</option>
</select> </select>
</div> </div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">User-Agent</span>
<span class="setting-hint">HTTP User-Agent for stream requests. Try VLC if your provider blocks
Chrome.</span>
</div>
<select id="setting-user-agent" class="source-select" style="width: auto; min-width: 200px;">
<option value="chrome">Chrome (Default)</option>
<option value="vlc">VLC</option>
<option value="tivimate">TiviMate</option>
<option value="custom">Custom...</option>
</select>
</div>
<div class="setting-item" id="custom-user-agent-container" style="display: none;">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Custom User-Agent</span>
<span class="setting-hint">Enter your own User-Agent string.</span>
</div>
<input type="text" id="setting-user-agent-custom" class="search-input" style="width: 300px;"
placeholder="Custom User-Agent string...">
</div>
</div> </div>
<div class="settings-section"> <div class="settings-section">
+45
View File
@@ -145,6 +145,39 @@ class SettingsPage {
this.app.player.saveSettings(); 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() { initUserManagement() {
@@ -297,6 +330,18 @@ class SettingsPage {
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false; if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24'; if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8'; 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 // Update EPG last refreshed display
+22 -2
View File
@@ -67,10 +67,30 @@ function getDefaultSettings() {
forceRemux: false, forceRemux: false,
autoTranscode: true, autoTranscode: true,
streamFormat: 'm3u8', 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 // Write lock to prevent concurrent writes from corrupting db.json
let writeQueue = Promise.resolve(); let writeQueue = Promise.resolve();
const tmpPath = dbPath + '.tmp'; 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 };
+11 -2
View File
@@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const db = require('../db');
/** /**
* Remux stream (container conversion only) * 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. * 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; const { url } = req.query;
if (!url) { if (!url) {
return res.status(400).json({ error: 'URL parameter is required' }); return res.status(400).json({ error: 'URL parameter is required' });
} }
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg'; 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] Starting remux for: ${url}`);
console.log(`[Remux] Using User-Agent: ${settings.userAgentPreset}`);
// FFmpeg arguments for pure remux (no encoding) // FFmpeg arguments for pure remux (no encoding)
// Very lightweight - just changes container from TS to fragmented MP4 // Very lightweight - just changes container from TS to fragmented MP4
const args = [ const args = [
'-hide_banner', '-hide_banner',
'-loglevel', 'warning', '-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 // Low-latency startup: reduce probe/analyze time for faster first bytes
'-probesize', '32768', '-probesize', '32768',
'-analyzeduration', '500000', // 0.5 seconds - enough to detect audio '-analyzeduration', '500000', // 0.5 seconds - enough to detect audio
@@ -40,6 +47,8 @@ router.get('/', (req, res) => {
'-reconnect', '1', '-reconnect', '1',
'-reconnect_streamed', '1', '-reconnect_streamed', '1',
'-reconnect_delay_max', '5', '-reconnect_delay_max', '5',
// Prevent Range/HEAD requests that some providers reject with 405
'-seekable', '0',
'-i', url, '-i', url,
// STRICT MAPPING: Only map video and audio, ignore subtitles/data/attachments // 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) // This prevents remux failure when source container has incompatible subtitle tracks (e.g. MKV -> MP4)
+11 -2
View File
@@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const db = require('../db');
/** /**
* Transcode stream * Transcode stream
@@ -9,14 +10,20 @@ const { spawn } = require('child_process');
* Transcodes audio to AAC for browser compatibility while passing video through. * 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. * 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; const { url } = req.query;
if (!url) { if (!url) {
return res.status(400).json({ error: 'URL parameter is required' }); return res.status(400).json({ error: 'URL parameter is required' });
} }
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg'; 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] Starting transcoding for: ${url}`);
console.log(`[Transcode] Using User-Agent: ${settings.userAgentPreset}`);
console.log(`[Transcode] Using binary: ${ffmpegPath}`); console.log(`[Transcode] Using binary: ${ffmpegPath}`);
// FFmpeg arguments for transcoding // FFmpeg arguments for transcoding
@@ -25,7 +32,7 @@ router.get('/', (req, res) => {
const args = [ const args = [
'-hide_banner', '-hide_banner',
'-loglevel', 'warning', '-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 // Faster startup - reduced probe/analyze for quicker first bytes
'-probesize', '2000000', // 2MB (reduced from 5MB) '-probesize', '2000000', // 2MB (reduced from 5MB)
'-analyzeduration', '3000000', // 3 seconds (reduced from 10s) '-analyzeduration', '3000000', // 3 seconds (reduced from 10s)
@@ -39,6 +46,8 @@ router.get('/', (req, res) => {
'-reconnect', '1', '-reconnect', '1',
'-reconnect_streamed', '1', '-reconnect_streamed', '1',
'-reconnect_delay_max', '3', '-reconnect_delay_max', '3',
// Prevent Range/HEAD requests that some providers reject with 405
'-seekable', '0',
'-i', url, '-i', url,
// Map only first video and audio stream (avoid subtitle streams causing issues) // Map only first video and audio stream (avoid subtitle streams causing issues)
'-map', '0:v:0', '-map', '0:v:0',