Added server-side saving for settings
This commit is contained in:
@@ -112,6 +112,14 @@ const API = {
|
|||||||
cache: {
|
cache: {
|
||||||
clear: (sourceId) => API.request('DELETE', `/proxy/cache/${sourceId}`)
|
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')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -981,7 +981,8 @@ class ChannelList {
|
|||||||
// Get stream URL
|
// Get stream URL
|
||||||
let streamUrl;
|
let streamUrl;
|
||||||
if (channel.sourceType === 'xtream') {
|
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);
|
const result = await API.proxy.xtream.getStreamUrl(channel.sourceId, channel.streamId, 'live', streamFormat);
|
||||||
streamUrl = result.url;
|
streamUrl = result.url;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,48 +15,83 @@ class VideoPlayer {
|
|||||||
this.overlayDuration = 5000; // 5 seconds
|
this.overlayDuration = 5000; // 5 seconds
|
||||||
this.isUsingProxy = false;
|
this.isUsingProxy = false;
|
||||||
this.currentUrl = null;
|
this.currentUrl = null;
|
||||||
|
this.settingsLoaded = false;
|
||||||
|
|
||||||
// Settings (loaded from localStorage)
|
// Settings - start with defaults, load from server async
|
||||||
this.settings = this.loadSettings();
|
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() {
|
loadSettings() {
|
||||||
const defaults = {
|
return this.settings;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+37
-17
@@ -105,29 +105,27 @@ class SettingsPage {
|
|||||||
|
|
||||||
// EPG refresh interval
|
// EPG refresh interval
|
||||||
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
|
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
|
||||||
if (epgRefreshSelect) {
|
if (epgRefreshSelect && this.app.player?.settings) {
|
||||||
// Load saved value
|
// Load saved value from player settings
|
||||||
const savedInterval = localStorage.getItem('nodecast_tv_epg_refresh_interval');
|
epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24';
|
||||||
if (savedInterval) {
|
|
||||||
epgRefreshSelect.value = savedInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save on change
|
// Save on change
|
||||||
epgRefreshSelect.addEventListener('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
|
// Stream output format
|
||||||
const streamFormatSelect = document.getElementById('setting-stream-format');
|
const streamFormatSelect = document.getElementById('setting-stream-format');
|
||||||
if (streamFormatSelect) {
|
if (streamFormatSelect && this.app.player?.settings) {
|
||||||
// Load saved value
|
// Load saved value from player settings
|
||||||
const savedFormat = localStorage.getItem('nodecast_tv_stream_format') || 'm3u8';
|
streamFormatSelect.value = this.app.player.settings.streamFormat || 'm3u8';
|
||||||
streamFormatSelect.value = savedFormat;
|
|
||||||
|
|
||||||
// Save on change
|
// Save on change
|
||||||
streamFormatSelect.addEventListener('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
|
// Load sources when page is shown
|
||||||
await this.app.sourceManager.loadSources();
|
await this.app.sourceManager.loadSources();
|
||||||
|
|
||||||
// Refresh player settings display
|
// Refresh ALL player settings from server
|
||||||
const arrowKeysToggle = document.getElementById('setting-arrow-keys');
|
|
||||||
const overlayDurationInput = document.getElementById('setting-overlay-duration');
|
|
||||||
if (this.app.player?.settings) {
|
if (this.app.player?.settings) {
|
||||||
arrowKeysToggle.checked = this.app.player.settings.arrowKeysChangeChannel;
|
const s = this.app.player.settings;
|
||||||
overlayDurationInput.value = this.app.player.settings.overlayDuration;
|
|
||||||
|
// 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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-2
@@ -21,6 +21,7 @@ async function loadDb() {
|
|||||||
sources: data.sources || [],
|
sources: data.sources || [],
|
||||||
hiddenItems: data.hiddenItems || [],
|
hiddenItems: data.hiddenItems || [],
|
||||||
favorites: data.favorites || [],
|
favorites: data.favorites || [],
|
||||||
|
settings: data.settings || getDefaultSettings(),
|
||||||
nextId: data.nextId || 1
|
nextId: data.nextId || 1
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -30,6 +31,7 @@ async function loadDb() {
|
|||||||
sources: [],
|
sources: [],
|
||||||
hiddenItems: [],
|
hiddenItems: [],
|
||||||
favorites: [],
|
favorites: [],
|
||||||
|
settings: getDefaultSettings(),
|
||||||
nextId: 1
|
nextId: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,13 +44,47 @@ async function loadDb() {
|
|||||||
sources: [],
|
sources: [],
|
||||||
hiddenItems: [],
|
hiddenItems: [],
|
||||||
favorites: [],
|
favorites: [],
|
||||||
|
settings: getDefaultSettings(),
|
||||||
nextId: 1
|
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) {
|
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
|
// 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 };
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ app.use('/api/channels', require('./routes/channels'));
|
|||||||
app.use('/api/favorites', require('./routes/favorites'));
|
app.use('/api/favorites', require('./routes/favorites'));
|
||||||
app.use('/api/transcode', require('./routes/transcode'));
|
app.use('/api/transcode', require('./routes/transcode'));
|
||||||
app.use('/api/remux', require('./routes/remux'));
|
app.use('/api/remux', require('./routes/remux'));
|
||||||
|
app.use('/api/settings', require('./routes/settings'));
|
||||||
|
|
||||||
// SPA fallback - serve index.html for all non-API routes
|
// SPA fallback - serve index.html for all non-API routes
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user