795a15cb8b
Lint & Test / test (push) Successful in 10s
- Abstract ReleaseProvider protocol for platform-agnostic version checking - GiteaReleaseProvider implementation using stdlib urllib - UpdateChecker service with periodic background checks and WS broadcast - Persistent dismissible banner in Web UI when a new version is detected - Health endpoint now returns cached update info - Configurable via update_check_enabled and update_check_interval settings - i18n support (EN/RU)
188 lines
6.1 KiB
JavaScript
188 lines
6.1 KiB
JavaScript
// ============================================================
|
|
// WebSocket: Connection, reconnection, authentication
|
|
// ============================================================
|
|
|
|
import {
|
|
dom, t, showToast, setWs,
|
|
WS_BACKOFF_BASE_MS, WS_BACKOFF_MAX_MS,
|
|
WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS,
|
|
authRequired, showUpdateBanner,
|
|
} from './core.js';
|
|
import { updateUI, visualizerEnabled, visualizerAvailable, setFrequencyData, stopPositionInterpolation, loadAudioDevices } from './player.js';
|
|
import { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js';
|
|
import { loadCallbacksTable } from './callbacks.js';
|
|
import { loadHeaderLinks, loadLinksTable } from './links.js';
|
|
|
|
let reconnectTimeout = null;
|
|
let pingInterval = null;
|
|
let wsReconnectAttempts = 0;
|
|
|
|
export function showAuthForm(errorMessage = '') {
|
|
const overlay = document.getElementById('auth-overlay');
|
|
overlay.classList.remove('hidden');
|
|
|
|
const errorEl = document.getElementById('auth-error');
|
|
if (errorMessage) {
|
|
errorEl.textContent = errorMessage;
|
|
errorEl.classList.add('visible');
|
|
} else {
|
|
errorEl.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
function hideAuthForm() {
|
|
document.getElementById('auth-overlay').classList.add('hidden');
|
|
}
|
|
|
|
export function authenticate() {
|
|
const token = document.getElementById('token-input').value.trim();
|
|
if (!token) {
|
|
showAuthForm(t('auth.required'));
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem('media_server_token', token);
|
|
connectWebSocket(token);
|
|
}
|
|
|
|
export function clearToken() {
|
|
localStorage.removeItem('media_server_token');
|
|
// Access ws via import
|
|
import('./core.js').then(core => {
|
|
if (core.ws) {
|
|
core.ws.close();
|
|
}
|
|
});
|
|
showAuthForm(t('auth.cleared'));
|
|
}
|
|
|
|
export function connectWebSocket(token) {
|
|
if (pingInterval) {
|
|
clearInterval(pingInterval);
|
|
pingInterval = null;
|
|
}
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsBase = `${protocol}//${window.location.host}/api/media/ws`;
|
|
const wsUrl = token ? `${wsBase}?token=${encodeURIComponent(token)}` : wsBase;
|
|
|
|
const newWs = new WebSocket(wsUrl);
|
|
setWs(newWs);
|
|
|
|
newWs.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
wsReconnectAttempts = 0;
|
|
updateConnectionStatus(true);
|
|
hideConnectionBanner();
|
|
hideAuthForm();
|
|
loadScripts();
|
|
loadScriptsTable();
|
|
loadCallbacksTable();
|
|
loadLinksTable();
|
|
loadHeaderLinks();
|
|
loadAudioDevices();
|
|
if (visualizerEnabled && visualizerAvailable) {
|
|
newWs.send(JSON.stringify({ type: 'enable_visualizer' }));
|
|
}
|
|
};
|
|
|
|
newWs.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
|
|
if (msg.type === 'status' || msg.type === 'status_update') {
|
|
updateUI(msg.data);
|
|
} else if (msg.type === 'scripts_changed') {
|
|
console.log('Scripts changed, reloading...');
|
|
loadScripts();
|
|
loadScriptsTable();
|
|
} else if (msg.type === 'links_changed') {
|
|
console.log('Links changed, reloading...');
|
|
loadHeaderLinks();
|
|
loadLinksTable();
|
|
displayQuickAccess();
|
|
} else if (msg.type === 'update_available') {
|
|
showUpdateBanner(msg.data);
|
|
} else if (msg.type === 'audio_data') {
|
|
setFrequencyData(msg.data);
|
|
} else if (msg.type === 'error') {
|
|
console.error('WebSocket error:', msg.message);
|
|
}
|
|
};
|
|
|
|
newWs.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
updateConnectionStatus(false);
|
|
};
|
|
|
|
newWs.onclose = (event) => {
|
|
console.log('WebSocket closed:', event.code);
|
|
updateConnectionStatus(false);
|
|
stopPositionInterpolation();
|
|
|
|
if (event.code === 4001) {
|
|
localStorage.removeItem('media_server_token');
|
|
showAuthForm(t('auth.invalid'));
|
|
} else if (event.code !== 1000) {
|
|
wsReconnectAttempts++;
|
|
|
|
if (wsReconnectAttempts <= WS_MAX_RECONNECT_ATTEMPTS) {
|
|
const delay = Math.min(
|
|
WS_BACKOFF_BASE_MS * Math.pow(1.5, wsReconnectAttempts - 1),
|
|
WS_BACKOFF_MAX_MS
|
|
);
|
|
console.log(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${wsReconnectAttempts}/${WS_MAX_RECONNECT_ATTEMPTS})...`);
|
|
|
|
if (wsReconnectAttempts >= 3) {
|
|
showConnectionBanner(t('connection.reconnecting').replace('{attempt}', wsReconnectAttempts), false);
|
|
}
|
|
|
|
reconnectTimeout = setTimeout(() => {
|
|
const savedToken = localStorage.getItem('media_server_token');
|
|
if (savedToken || !authRequired) {
|
|
connectWebSocket(savedToken || '');
|
|
}
|
|
}, delay);
|
|
} else {
|
|
showConnectionBanner(t('connection.lost'), true);
|
|
}
|
|
}
|
|
};
|
|
|
|
pingInterval = setInterval(() => {
|
|
if (newWs && newWs.readyState === WebSocket.OPEN) {
|
|
newWs.send(JSON.stringify({ type: 'ping' }));
|
|
}
|
|
}, WS_PING_INTERVAL_MS);
|
|
}
|
|
|
|
export function updateConnectionStatus(connected) {
|
|
if (connected) {
|
|
dom.statusDot.classList.add('connected');
|
|
} else {
|
|
dom.statusDot.classList.remove('connected');
|
|
}
|
|
}
|
|
|
|
function showConnectionBanner(message, showButton) {
|
|
const banner = document.getElementById('connectionBanner');
|
|
const text = document.getElementById('connectionBannerText');
|
|
const btn = document.getElementById('connectionBannerBtn');
|
|
text.textContent = message;
|
|
btn.style.display = showButton ? '' : 'none';
|
|
banner.classList.remove('hidden');
|
|
}
|
|
|
|
function hideConnectionBanner() {
|
|
const banner = document.getElementById('connectionBanner');
|
|
banner.classList.add('hidden');
|
|
}
|
|
|
|
export function manualReconnect() {
|
|
const savedToken = localStorage.getItem('media_server_token');
|
|
if (savedToken || !authRequired) {
|
|
wsReconnectAttempts = 0;
|
|
hideConnectionBanner();
|
|
connectWebSocket(savedToken || '');
|
|
}
|
|
}
|