Refactored EPG refresh logic
This commit is contained in:
+16
-9
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -124,27 +125,29 @@
|
|||||||
<section class="player-section">
|
<section class="player-section">
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<video id="video-player" controls playsinline></video>
|
<video id="video-player" controls playsinline></video>
|
||||||
|
|
||||||
<!-- Custom Mobile Controls -->
|
<!-- Custom Mobile Controls -->
|
||||||
<div class="video-controls" id="video-controls">
|
<div class="video-controls" id="video-controls">
|
||||||
<button class="video-control-btn" id="btn-play" title="Play/Pause">
|
<button class="video-control-btn" id="btn-play" title="Play/Pause">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||||
<path d="M8 5v14l11-7z"/>
|
<path d="M8 5v14l11-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="video-control-btn" id="btn-mute" title="Mute/Unmute">
|
<button class="video-control-btn" id="btn-mute" title="Mute/Unmute">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
<path
|
||||||
|
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="video-control-btn" id="btn-pip" title="Picture in Picture">
|
<button class="video-control-btn" id="btn-pip" title="Picture in Picture">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||||
<path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/>
|
<path
|
||||||
|
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="video-control-btn" id="btn-fullscreen" title="Fullscreen">
|
<button class="video-control-btn" id="btn-fullscreen" title="Fullscreen">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +325,11 @@
|
|||||||
<option value="0">Manual only</option>
|
<option value="0">Manual only</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">Use the 🔄 button next to EPG sources for manual refresh</p>
|
<p class="hint">Use the <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||||
|
class="icon" style="width: 1em; height: 1em; vertical-align: middle;">
|
||||||
|
<path
|
||||||
|
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
|
||||||
|
</svg> button next to EPG sources for manual refresh</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -482,7 +489,7 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>User Management</h3>
|
<h3>User Management</h3>
|
||||||
<p class="hint">Manage user accounts and permissions</p>
|
<p class="hint">Manage user accounts and permissions</p>
|
||||||
|
|
||||||
<div class="user-list-container">
|
<div class="user-list-container">
|
||||||
<table class="user-table">
|
<table class="user-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -500,7 +507,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-user-section">
|
<div class="add-user-section">
|
||||||
<h4>Add New User</h4>
|
<h4>Add New User</h4>
|
||||||
<form id="add-user-form" class="user-form">
|
<form id="add-user-form" class="user-form">
|
||||||
|
|||||||
@@ -122,11 +122,83 @@ class EpgGuide {
|
|||||||
getRefreshInterval() {
|
getRefreshInterval() {
|
||||||
// Read from server-side player settings (synced via Settings page)
|
// Read from server-side player settings (synced via Settings page)
|
||||||
if (window.app?.player?.settings?.epgRefreshInterval) {
|
if (window.app?.player?.settings?.epgRefreshInterval) {
|
||||||
return parseInt(window.app.player.settings.epgRefreshInterval);
|
return parseFloat(window.app.player.settings.epgRefreshInterval);
|
||||||
}
|
}
|
||||||
return 24; // Default 24 hours
|
return 24; // Default 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start background EPG refresh timer
|
||||||
|
* Automatically fetches fresh EPG data at the configured interval
|
||||||
|
*/
|
||||||
|
startBackgroundRefresh() {
|
||||||
|
// Clear any existing timer
|
||||||
|
this.stopBackgroundRefresh();
|
||||||
|
|
||||||
|
const intervalHours = this.getRefreshInterval();
|
||||||
|
|
||||||
|
// If interval is 0 or invalid, don't start timer (manual refresh only)
|
||||||
|
if (!intervalHours || intervalHours <= 0) {
|
||||||
|
console.log('[EPG] Background refresh disabled (manual only mode)');
|
||||||
|
this._currentRefreshInterval = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalMs = intervalHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
||||||
|
|
||||||
|
console.log(`[EPG] Starting background refresh timer: every ${intervalHours} hours (${Math.round(intervalMs / 1000)}s)`);
|
||||||
|
|
||||||
|
this._backgroundRefreshTimer = setInterval(async () => {
|
||||||
|
console.log('[EPG] Background refresh triggered');
|
||||||
|
try {
|
||||||
|
await this.fetchEpgData(true); // Force refresh
|
||||||
|
this.lastRefreshTime = new Date();
|
||||||
|
console.log('[EPG] Background refresh complete');
|
||||||
|
|
||||||
|
// Update channel list program info if visible
|
||||||
|
if (window.app?.channelList) {
|
||||||
|
window.app.channelList.clearProgramInfoCache();
|
||||||
|
window.app.channelList.updateVisibleEpgInfo?.();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[EPG] Background refresh failed:', err);
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
// Store current interval so we can detect changes
|
||||||
|
this._currentRefreshInterval = intervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop background EPG refresh timer
|
||||||
|
*/
|
||||||
|
stopBackgroundRefresh() {
|
||||||
|
if (this._backgroundRefreshTimer) {
|
||||||
|
clearInterval(this._backgroundRefreshTimer);
|
||||||
|
this._backgroundRefreshTimer = null;
|
||||||
|
console.log('[EPG] Background refresh timer stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart background refresh if interval has changed
|
||||||
|
* Called when settings change
|
||||||
|
*/
|
||||||
|
restartBackgroundRefreshIfNeeded() {
|
||||||
|
const newInterval = this.getRefreshInterval();
|
||||||
|
if (this._currentRefreshInterval !== newInterval) {
|
||||||
|
console.log(`[EPG] Refresh interval changed: ${this._currentRefreshInterval}h -> ${newInterval}h`);
|
||||||
|
this.startBackgroundRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last refresh time for display
|
||||||
|
*/
|
||||||
|
getLastRefreshTime() {
|
||||||
|
return this.lastRefreshTime || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load EPG data (server-side caching)
|
* Load EPG data (server-side caching)
|
||||||
*/
|
*/
|
||||||
@@ -134,7 +206,12 @@ class EpgGuide {
|
|||||||
try {
|
try {
|
||||||
this.container.innerHTML = '<div class="loading"></div>';
|
this.container.innerHTML = '<div class="loading"></div>';
|
||||||
await this.fetchEpgData(forceRefresh);
|
await this.fetchEpgData(forceRefresh);
|
||||||
|
this.lastRefreshTime = new Date();
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
|
// Start background refresh timer after initial load
|
||||||
|
// This ensures EPG data stays fresh while the app is open
|
||||||
|
this.startBackgroundRefresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading EPG:', err);
|
console.error('Error loading EPG:', err);
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
|
|||||||
+19
-14
@@ -19,7 +19,7 @@ class SettingsPage {
|
|||||||
|
|
||||||
// Player settings
|
// Player settings
|
||||||
this.initPlayerSettings();
|
this.initPlayerSettings();
|
||||||
|
|
||||||
// User management (admin only)
|
// User management (admin only)
|
||||||
this.initUserManagement();
|
this.initUserManagement();
|
||||||
}
|
}
|
||||||
@@ -112,10 +112,15 @@ class SettingsPage {
|
|||||||
// Load saved value from player settings
|
// Load saved value from player settings
|
||||||
epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24';
|
epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24';
|
||||||
|
|
||||||
// Save on change
|
// Save on change and restart background timer
|
||||||
epgRefreshSelect.addEventListener('change', () => {
|
epgRefreshSelect.addEventListener('change', () => {
|
||||||
this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value;
|
this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value;
|
||||||
this.app.player.saveSettings();
|
this.app.player.saveSettings();
|
||||||
|
|
||||||
|
// Restart EPG background refresh with new interval
|
||||||
|
if (window.app?.epgGuide) {
|
||||||
|
window.app.epgGuide.restartBackgroundRefreshIfNeeded();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,17 +141,17 @@ class SettingsPage {
|
|||||||
initUserManagement() {
|
initUserManagement() {
|
||||||
// User tab visibility is handled in show() method
|
// User tab visibility is handled in show() method
|
||||||
// when currentUser is available
|
// when currentUser is available
|
||||||
|
|
||||||
// Handle add user form
|
// Handle add user form
|
||||||
const addUserForm = document.getElementById('add-user-form');
|
const addUserForm = document.getElementById('add-user-form');
|
||||||
if (addUserForm) {
|
if (addUserForm) {
|
||||||
addUserForm.addEventListener('submit', async (e) => {
|
addUserForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const username = document.getElementById('new-username').value;
|
const username = document.getElementById('new-username').value;
|
||||||
const password = document.getElementById('new-password').value;
|
const password = document.getElementById('new-password').value;
|
||||||
const role = document.getElementById('new-role').value;
|
const role = document.getElementById('new-role').value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.users.create({ username, password, role });
|
await API.users.create({ username, password, role });
|
||||||
alert('User created successfully!');
|
alert('User created successfully!');
|
||||||
@@ -162,15 +167,15 @@ class SettingsPage {
|
|||||||
async loadUsers() {
|
async loadUsers() {
|
||||||
const userList = document.getElementById('user-list');
|
const userList = document.getElementById('user-list');
|
||||||
if (!userList) return;
|
if (!userList) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const users = await API.users.getAll();
|
const users = await API.users.getAll();
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
userList.innerHTML = '<tr><td colspan="4" class="hint">No users found</td></tr>';
|
userList.innerHTML = '<tr><td colspan="4" class="hint">No users found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userList.innerHTML = users.map(user => `
|
userList.innerHTML = users.map(user => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${user.username}</td>
|
<td>${user.username}</td>
|
||||||
@@ -192,17 +197,17 @@ class SettingsPage {
|
|||||||
const username = prompt('Enter new username (leave blank to keep current):');
|
const username = prompt('Enter new username (leave blank to keep current):');
|
||||||
const password = prompt('Enter new password (leave blank to keep current):');
|
const password = prompt('Enter new password (leave blank to keep current):');
|
||||||
const role = prompt('Enter role (admin or viewer, leave blank to keep current):');
|
const role = prompt('Enter role (admin or viewer, leave blank to keep current):');
|
||||||
|
|
||||||
const updates = {};
|
const updates = {};
|
||||||
if (username) updates.username = username;
|
if (username) updates.username = username;
|
||||||
if (password) updates.password = password;
|
if (password) updates.password = password;
|
||||||
if (role) updates.role = role;
|
if (role) updates.role = role;
|
||||||
|
|
||||||
if (Object.keys(updates).length === 0) {
|
if (Object.keys(updates).length === 0) {
|
||||||
alert('No changes made');
|
alert('No changes made');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.users.update(userId, updates);
|
await API.users.update(userId, updates);
|
||||||
alert('User updated successfully!');
|
alert('User updated successfully!');
|
||||||
@@ -216,7 +221,7 @@ class SettingsPage {
|
|||||||
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
|
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.users.delete(userId);
|
await API.users.delete(userId);
|
||||||
alert('User deleted successfully!');
|
alert('User deleted successfully!');
|
||||||
@@ -234,7 +239,7 @@ class SettingsPage {
|
|||||||
if (tabName === 'content') {
|
if (tabName === 'content') {
|
||||||
this.app.sourceManager.loadContentSources();
|
this.app.sourceManager.loadContentSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load users when switching to users tab
|
// Load users when switching to users tab
|
||||||
if (tabName === 'users') {
|
if (tabName === 'users') {
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
@@ -249,7 +254,7 @@ class SettingsPage {
|
|||||||
usersTab.style.display = 'block';
|
usersTab.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sources when page is shown
|
// Load sources when page is shown
|
||||||
await this.app.sourceManager.loadSources();
|
await this.app.sourceManager.loadSources();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user