From d1bfd1b9e7ca82c5e2726e41f85d14ddd2a6d4f5 Mon Sep 17 00:00:00 2001 From: Trevor Mears Date: Wed, 31 Dec 2025 15:35:37 -0800 Subject: [PATCH] Added server-side saving for settings --- public/js/api.js | 8 +++ public/js/components/ChannelList.js | 3 +- public/js/components/VideoPlayer.js | 105 ++++++++++++++++++---------- public/js/pages/Settings.js | 54 +++++++++----- server/db.js | 62 +++++++++++++++- server/index.js | 1 + server/routes/settings.js | 56 +++++++++++++++ 7 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 server/routes/settings.js diff --git a/public/js/api.js b/public/js/api.js index ba0871d..dfaae9c 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -112,6 +112,14 @@ const API = { cache: { clear: (sourceId) => API.request('DELETE', `/proxy/cache/${sourceId}`) } + }, + + // Settings + settings: { + get: () => API.request('GET', '/settings'), + update: (data) => API.request('PUT', '/settings', data), + reset: () => API.request('DELETE', '/settings'), + getDefaults: () => API.request('GET', '/settings/defaults') } }; diff --git a/public/js/components/ChannelList.js b/public/js/components/ChannelList.js index 6cdac0e..207b8b3 100644 --- a/public/js/components/ChannelList.js +++ b/public/js/components/ChannelList.js @@ -981,7 +981,8 @@ class ChannelList { // Get stream URL let streamUrl; if (channel.sourceType === 'xtream') { - const streamFormat = localStorage.getItem('nodecast_tv_stream_format') || 'm3u8'; + // Get stream format from player settings (server-side) or fallback + const streamFormat = window.app?.player?.settings?.streamFormat || 'm3u8'; const result = await API.proxy.xtream.getStreamUrl(channel.sourceId, channel.streamId, 'live', streamFormat); streamUrl = result.url; } else { diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index 94ffcca..5be85b5 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -15,48 +15,83 @@ class VideoPlayer { this.overlayDuration = 5000; // 5 seconds this.isUsingProxy = false; this.currentUrl = null; + this.settingsLoaded = false; - // Settings (loaded from localStorage) - this.settings = this.loadSettings(); + // Settings - start with defaults, load from server async + this.settings = this.getDefaultSettings(); - this.init(); + // Load settings from server, then init + this.loadSettingsFromServer().then(() => { + this.init(); + }); } /** - * Load settings from localStorage + * Default settings + */ + getDefaultSettings() { + return { + arrowKeysChangeChannel: true, + overlayDuration: 5, + defaultVolume: 80, + rememberVolume: true, + lastVolume: 80, + autoPlayNextEpisode: false, + forceProxy: false, + forceTranscode: false, + forceRemux: false, + streamFormat: 'm3u8', + epgRefreshInterval: '24' + }; + } + + /** + * Load settings from server API + */ + async loadSettingsFromServer() { + try { + const serverSettings = await API.settings.get(); + this.settings = { ...this.getDefaultSettings(), ...serverSettings }; + this.settingsLoaded = true; + console.log('[Player] Settings loaded from server'); + } catch (err) { + console.warn('[Player] Failed to load settings from server, using defaults:', err.message); + // Fall back to localStorage for backwards compatibility + try { + const saved = localStorage.getItem('nodecast_tv_player_settings'); + if (saved) { + this.settings = { ...this.getDefaultSettings(), ...JSON.parse(saved) }; + console.log('[Player] Settings loaded from localStorage (fallback)'); + } + } catch (localErr) { + console.error('[Player] Error loading localStorage settings:', localErr); + } + } + } + + /** + * Save settings to server API + */ + async saveSettings() { + try { + await API.settings.update(this.settings); + console.log('[Player] Settings saved to server'); + } catch (err) { + console.error('[Player] Error saving settings to server:', err); + // Also save to localStorage as backup + try { + localStorage.setItem('nodecast_tv_player_settings', JSON.stringify(this.settings)); + } catch (localErr) { + console.error('[Player] Error saving to localStorage:', localErr); + } + } + } + + /** + * Legacy sync method for compatibility - calls async version */ loadSettings() { - const defaults = { - arrowKeysChangeChannel: true, // Use arrow keys for channel navigation - overlayDuration: 5, // Seconds - defaultVolume: 80, // 0-100 - rememberVolume: true, // Persist volume between sessions - lastVolume: 80, // Last used volume (if rememberVolume is true) - autoPlayNextEpisode: false, // Auto-play next episode in series - forceProxy: false, // Force all streams through backend proxy - forceTranscode: false, // Force audio transcoding (fixes Dolby/AC3 audio) - forceRemux: false // Force remux to MP4 (fixes raw .ts streams, lightweight) - }; - try { - const saved = localStorage.getItem('nodecast_tv_player_settings'); - if (saved) { - return { ...defaults, ...JSON.parse(saved) }; - } - } catch (err) { - console.error('Error loading settings:', err); - } - return defaults; - } - - /** - * Save settings to localStorage - */ - saveSettings() { - try { - localStorage.setItem('nodecast_tv_player_settings', JSON.stringify(this.settings)); - } catch (err) { - console.error('Error saving settings:', err); - } + return this.settings; } /** diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js index c4e4bb7..b61e238 100644 --- a/public/js/pages/Settings.js +++ b/public/js/pages/Settings.js @@ -105,29 +105,27 @@ class SettingsPage { // EPG refresh interval const epgRefreshSelect = document.getElementById('epg-refresh-interval'); - if (epgRefreshSelect) { - // Load saved value - const savedInterval = localStorage.getItem('nodecast_tv_epg_refresh_interval'); - if (savedInterval) { - epgRefreshSelect.value = savedInterval; - } + if (epgRefreshSelect && this.app.player?.settings) { + // Load saved value from player settings + epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24'; // Save on change epgRefreshSelect.addEventListener('change', () => { - localStorage.setItem('nodecast_tv_epg_refresh_interval', epgRefreshSelect.value); + this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value; + this.app.player.saveSettings(); }); } // Stream output format const streamFormatSelect = document.getElementById('setting-stream-format'); - if (streamFormatSelect) { - // Load saved value - const savedFormat = localStorage.getItem('nodecast_tv_stream_format') || 'm3u8'; - streamFormatSelect.value = savedFormat; + if (streamFormatSelect && this.app.player?.settings) { + // Load saved value from player settings + streamFormatSelect.value = this.app.player.settings.streamFormat || 'm3u8'; // Save on change streamFormatSelect.addEventListener('change', () => { - localStorage.setItem('nodecast_tv_stream_format', streamFormatSelect.value); + this.app.player.settings.streamFormat = streamFormatSelect.value; + this.app.player.saveSettings(); }); } } @@ -146,12 +144,34 @@ class SettingsPage { // Load sources when page is shown await this.app.sourceManager.loadSources(); - // Refresh player settings display - const arrowKeysToggle = document.getElementById('setting-arrow-keys'); - const overlayDurationInput = document.getElementById('setting-overlay-duration'); + // Refresh ALL player settings from server if (this.app.player?.settings) { - arrowKeysToggle.checked = this.app.player.settings.arrowKeysChangeChannel; - overlayDurationInput.value = this.app.player.settings.overlayDuration; + const s = this.app.player.settings; + + // Player settings + const arrowKeysToggle = document.getElementById('setting-arrow-keys'); + const overlayDurationInput = document.getElementById('setting-overlay-duration'); + const defaultVolumeSlider = document.getElementById('setting-default-volume'); + const volumeValueDisplay = document.getElementById('volume-value'); + const rememberVolumeToggle = document.getElementById('setting-remember-volume'); + const autoPlayNextToggle = document.getElementById('setting-autoplay-next'); + const forceProxyToggle = document.getElementById('setting-force-proxy'); + const forceTranscodeToggle = document.getElementById('setting-force-transcode'); + const forceRemuxToggle = document.getElementById('setting-force-remux'); + const epgRefreshSelect = document.getElementById('epg-refresh-interval'); + const streamFormatSelect = document.getElementById('setting-stream-format'); + + if (arrowKeysToggle) arrowKeysToggle.checked = s.arrowKeysChangeChannel; + if (overlayDurationInput) overlayDurationInput.value = s.overlayDuration; + if (defaultVolumeSlider) defaultVolumeSlider.value = s.defaultVolume; + if (volumeValueDisplay) volumeValueDisplay.textContent = s.defaultVolume + '%'; + if (rememberVolumeToggle) rememberVolumeToggle.checked = s.rememberVolume; + if (autoPlayNextToggle) autoPlayNextToggle.checked = s.autoPlayNextEpisode; + if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy || false; + if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode || false; + if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false; + if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24'; + if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8'; } } diff --git a/server/db.js b/server/db.js index 7a50ce6..999e599 100644 --- a/server/db.js +++ b/server/db.js @@ -21,6 +21,7 @@ async function loadDb() { sources: data.sources || [], hiddenItems: data.hiddenItems || [], favorites: data.favorites || [], + settings: data.settings || getDefaultSettings(), nextId: data.nextId || 1 }; } catch (error) { @@ -30,6 +31,7 @@ async function loadDb() { sources: [], hiddenItems: [], favorites: [], + settings: getDefaultSettings(), nextId: 1 }; } @@ -42,13 +44,47 @@ async function loadDb() { sources: [], hiddenItems: [], favorites: [], + settings: getDefaultSettings(), nextId: 1 }; } } +// Default settings +function getDefaultSettings() { + return { + arrowKeysChangeChannel: true, + overlayDuration: 5, + defaultVolume: 80, + rememberVolume: true, + lastVolume: 80, + autoPlayNextEpisode: false, + forceProxy: false, + forceTranscode: false, + forceRemux: false, + streamFormat: 'm3u8', + epgRefreshInterval: '24' + }; +} + +// Write lock to prevent concurrent writes from corrupting db.json +let writeQueue = Promise.resolve(); + async function saveDb(data) { - await fs.writeFile(dbPath, JSON.stringify(data, null, 2)); + // Queue this write operation - each write waits for the previous one + writeQueue = writeQueue.then(async () => { + try { + const jsonString = JSON.stringify(data, null, 2); + await fs.writeFile(dbPath, jsonString); + } catch (err) { + console.error('Error writing database:', err); + throw err; + } + }).catch(err => { + console.error('Database write failed:', err); + }); + + return writeQueue; } // Source CRUD operations @@ -253,4 +289,26 @@ const favorites = { } }; -module.exports = { sources, hiddenItems, favorites }; +// Settings operations +const settings = { + async get() { + const db = await loadDb(); + return { ...getDefaultSettings(), ...db.settings }; + }, + + async update(newSettings) { + const db = await loadDb(); + db.settings = { ...db.settings, ...newSettings }; + await saveDb(db); + return db.settings; + }, + + async reset() { + const db = await loadDb(); + db.settings = getDefaultSettings(); + await saveDb(db); + return db.settings; + } +}; + +module.exports = { sources, hiddenItems, favorites, settings, getDefaultSettings }; diff --git a/server/index.js b/server/index.js index ca1b633..c542ed8 100644 --- a/server/index.js +++ b/server/index.js @@ -55,6 +55,7 @@ app.use('/api/channels', require('./routes/channels')); app.use('/api/favorites', require('./routes/favorites')); app.use('/api/transcode', require('./routes/transcode')); app.use('/api/remux', require('./routes/remux')); +app.use('/api/settings', require('./routes/settings')); // SPA fallback - serve index.html for all non-API routes app.get('*', (req, res) => { diff --git a/server/routes/settings.js b/server/routes/settings.js new file mode 100644 index 0000000..607193a --- /dev/null +++ b/server/routes/settings.js @@ -0,0 +1,56 @@ +const express = require('express'); +const router = express.Router(); +const { settings, getDefaultSettings } = require('../db'); + +/** + * Get all settings + * GET /api/settings + */ +router.get('/', async (req, res) => { + try { + const currentSettings = await settings.get(); + res.json(currentSettings); + } catch (err) { + console.error('Error getting settings:', err); + res.status(500).json({ error: err.message }); + } +}); + +/** + * Update settings (partial update) + * PUT /api/settings + */ +router.put('/', async (req, res) => { + try { + const updates = req.body; + const updatedSettings = await settings.update(updates); + res.json(updatedSettings); + } catch (err) { + console.error('Error updating settings:', err); + res.status(500).json({ error: err.message }); + } +}); + +/** + * Reset settings to defaults + * DELETE /api/settings + */ +router.delete('/', async (req, res) => { + try { + const defaultSettings = await settings.reset(); + res.json(defaultSettings); + } catch (err) { + console.error('Error resetting settings:', err); + res.status(500).json({ error: err.message }); + } +}); + +/** + * Get default settings (for reference) + * GET /api/settings/defaults + */ +router.get('/defaults', (req, res) => { + res.json(getDefaultSettings()); +}); + +module.exports = router;