Compare commits
2
Commits
52a643a868
...
aef63ca444
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef63ca444 | ||
|
|
10d15dfddc |
@@ -929,14 +929,17 @@
|
|||||||
|
|
||||||
<!-- Sidebar: Status -->
|
<!-- Sidebar: Status -->
|
||||||
<div class="sidebar" style="min-width: 200px; display:flex; flex-direction:column; height:100%;">
|
<div class="sidebar" style="min-width: 200px; display:flex; flex-direction:column; height:100%;">
|
||||||
<!-- Version Control (Hidden since we don't use it anymore) -->
|
<!-- Station Update -->
|
||||||
<div class="version-panel" style="display:none;">
|
<div class="version-panel">
|
||||||
<div class="version-panel-header">
|
<div class="version-panel-header">
|
||||||
<span>Audio Version</span>
|
<span>Station Update</span>
|
||||||
<button class="btn-icon" onclick="fetchVersion()" title="Refresh Version">
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button class="btn" id="publish-update-btn" onclick="publishStationUpdate()" title="Publish Station Update">发布更新</button>
|
||||||
|
<button class="btn-icon" onclick="fetchStationUpdate()" title="Refresh Update Status">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="version-display" id="current-version">Loading...</div>
|
<div class="version-display" id="current-version">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1072,6 +1075,7 @@
|
|||||||
let currentPath = 'root';
|
let currentPath = 'root';
|
||||||
let stationCodeMap = {};
|
let stationCodeMap = {};
|
||||||
let broadcastingStations = new Set();
|
let broadcastingStations = new Set();
|
||||||
|
let currentStationUpdate = null;
|
||||||
|
|
||||||
// --- WebSocket ---
|
// --- WebSocket ---
|
||||||
const connStatus = document.getElementById('conn-status');
|
const connStatus = document.getElementById('conn-status');
|
||||||
@@ -1118,6 +1122,11 @@
|
|||||||
|
|
||||||
// --- Renderers ---
|
// --- Renderers ---
|
||||||
function updateState(state) {
|
function updateState(state) {
|
||||||
|
if (state.stationUpdate) {
|
||||||
|
currentStationUpdate = state.stationUpdate;
|
||||||
|
renderStationUpdate(state.stationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.stations) {
|
if (state.stations) {
|
||||||
const list = document.getElementById('station-list');
|
const list = document.getElementById('station-list');
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
@@ -1136,6 +1145,12 @@
|
|||||||
const hasCC = Array.isArray(st.tags) && st.tags.includes('CC');
|
const hasCC = Array.isArray(st.tags) && st.tags.includes('CC');
|
||||||
const stationTarget = st.target || st.name;
|
const stationTarget = st.target || st.name;
|
||||||
const memberCount = Number(st.memberCount || 1);
|
const memberCount = Number(st.memberCount || 1);
|
||||||
|
const stationVersion = st.version || '未知版本';
|
||||||
|
const targetVersion = currentStationUpdate?.targetVersion || '';
|
||||||
|
const needsUpdate = !!targetVersion && stationVersion !== targetVersion;
|
||||||
|
const updateBadge = needsUpdate
|
||||||
|
? `<span style="font-size:0.65rem; background:rgba(245, 158, 11, 0.12); color:#fbbf24; padding:1px 5px; border-radius:4px; margin-left:6px; font-weight:normal; border:1px solid rgba(245, 158, 11, 0.25)">待更新 ${targetVersion}</span>`
|
||||||
|
: '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'status-item';
|
div.className = 'status-item';
|
||||||
div.id = `station-item-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
div.id = `station-item-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||||
@@ -1145,8 +1160,9 @@
|
|||||||
<div class="item-title">
|
<div class="item-title">
|
||||||
${st.name}
|
${st.name}
|
||||||
${hasCC ? '<span style="font-size:0.65rem; background:rgba(16, 185, 129, 0.1); color:#34d399; padding:1px 5px; border-radius:4px; margin-left:6px; font-weight:normal; border:1px solid rgba(16, 185, 129, 0.2)">CC</span>' : ''}
|
${hasCC ? '<span style="font-size:0.65rem; background:rgba(16, 185, 129, 0.1); color:#34d399; padding:1px 5px; border-radius:4px; margin-left:6px; font-weight:normal; border:1px solid rgba(16, 185, 129, 0.2)">CC</span>' : ''}
|
||||||
|
${updateBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-sub">${st.code}${st.platform ? ` - Platform ${st.platform}` : ''}${memberCount > 1 ? ` · Nodes ${memberCount}` : ''}</div>
|
<div class="item-sub">${st.code}${st.platform ? ` - Platform ${st.platform}` : ''}${memberCount > 1 ? ` · Nodes ${memberCount}` : ''} · ${stationVersion}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:10px">
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
<div class="status-dot ${dotClass}" id="station-dot-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}" data-online="${isOnline}" data-name="${st.name}"></div>
|
<div class="status-dot ${dotClass}" id="station-dot-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}" data-online="${isOnline}" data-name="${st.name}"></div>
|
||||||
@@ -1688,20 +1704,54 @@
|
|||||||
|
|
||||||
// Init
|
// Init
|
||||||
refreshFiles();
|
refreshFiles();
|
||||||
fetchVersion();
|
fetchStationUpdate();
|
||||||
|
|
||||||
async function fetchVersion() {
|
function renderStationUpdate(info) {
|
||||||
try {
|
const el = document.getElementById('current-version');
|
||||||
const res = await fetch('/api/version');
|
const localVersion = info?.localVersion || '未知';
|
||||||
const data = await res.json();
|
const targetVersion = info?.targetVersion || '未发布';
|
||||||
if (data.version) {
|
const publishedAt = info?.publishedAt ? new Date(info.publishedAt).toLocaleString() : '未发布';
|
||||||
document.getElementById('current-version').textContent = data.version;
|
el.innerHTML = `
|
||||||
|
<div>服务器脚本:${localVersion}</div>
|
||||||
|
<div>已发布版本:${targetVersion}</div>
|
||||||
|
<div style="margin-top:4px;color:var(--text-muted)">发布时间:${publishedAt}</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchStationUpdate() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/station-update');
|
||||||
|
const data = await res.json();
|
||||||
|
currentStationUpdate = data;
|
||||||
|
renderStationUpdate(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('current-version').textContent = 'Error';
|
document.getElementById('current-version').textContent = 'Error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function publishStationUpdate() {
|
||||||
|
if (!confirm('发布后,所有在线主机会立即下载最新 startup 并自动重启;离线主机会在下次加载时立即更新。确定继续吗?')) return;
|
||||||
|
|
||||||
|
const btn = document.getElementById('publish-update-btn');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '发布中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/station-updates/publish', { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) throw new Error(data.message || 'Publish failed');
|
||||||
|
currentStationUpdate = data;
|
||||||
|
renderStationUpdate(data);
|
||||||
|
showToast('更新已发布', `目标版本 ${data.targetVersion || data.localVersion},已通知 ${data.notifiedStations || 0}/${data.onlineStations || 0} 台在线主机。`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`发布失败:${e.message || e}`);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Resizers ---
|
// --- Resizers ---
|
||||||
const resizerV = document.getElementById('resizer-v');
|
const resizerV = document.getElementById('resizer-v');
|
||||||
const resizerH = document.getElementById('resizer-h');
|
const resizerH = document.getElementById('resizer-h');
|
||||||
|
|||||||
+147
-4
@@ -49,6 +49,10 @@ const audioGeneralLegacyDir = path.join(audioRootDir, 'general');
|
|||||||
const audioStationDir = path.join(audioRootDir, 'station');
|
const audioStationDir = path.join(audioRootDir, 'station');
|
||||||
const listFilePath = path.join(appRootDir, 'list.txt');
|
const listFilePath = path.join(appRootDir, 'list.txt');
|
||||||
const tempBroadcastDir = path.join(appRootDir, 'temp-broadcast');
|
const tempBroadcastDir = path.join(appRootDir, 'temp-broadcast');
|
||||||
|
const stationScriptPath = path.join(appRootDir, 'station_broadcast.lua');
|
||||||
|
const stationUpdateScriptPath = path.join(appRootDir, 'update_station_broadcast.lua');
|
||||||
|
const STATION_SCRIPT_DOWNLOAD_PATH = '/downloads/station_broadcast.lua';
|
||||||
|
const STATION_UPDATE_SCRIPT_DOWNLOAD_PATH = '/downloads/update_station_broadcast.lua';
|
||||||
const STREAMABLE_AUDIO_EXTENSIONS = ['.wav', '.mp3', '.ogg', '.flac', '.m4a', '.aac', '.opus'];
|
const STREAMABLE_AUDIO_EXTENSIONS = ['.wav', '.mp3', '.ogg', '.flac', '.m4a', '.aac', '.opus'];
|
||||||
const MUSIC_AUDIO_EXTENSIONS = [...STREAMABLE_AUDIO_EXTENSIONS, '.dfpwm'];
|
const MUSIC_AUDIO_EXTENSIONS = [...STREAMABLE_AUDIO_EXTENSIONS, '.dfpwm'];
|
||||||
|
|
||||||
@@ -190,6 +194,11 @@ let activeTrains = [];
|
|||||||
let stationLogs = [];
|
let stationLogs = [];
|
||||||
let audioVersion = String(Date.now());
|
let audioVersion = String(Date.now());
|
||||||
let versions = [];
|
let versions = [];
|
||||||
|
let stationUpdate = {
|
||||||
|
version: '',
|
||||||
|
releaseId: '',
|
||||||
|
publishedAt: 0
|
||||||
|
};
|
||||||
|
|
||||||
const DATA_FILE = path.join(__dirname, 'server-data.json');
|
const DATA_FILE = path.join(__dirname, 'server-data.json');
|
||||||
|
|
||||||
@@ -199,6 +208,94 @@ function delay(ms) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLocalStationScriptVersion() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(stationScriptPath)) return '';
|
||||||
|
const content = fs.readFileSync(stationScriptPath, 'utf8');
|
||||||
|
const match = content.match(/local\s+APP_VERSION_TEXT\s*=\s*["']([^"']+)["']/);
|
||||||
|
return match ? String(match[1]).trim() : '';
|
||||||
|
} catch (e) {
|
||||||
|
log('ERROR', `Failed to read station script version: ${e.message}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAbsoluteUrl(req, pathname) {
|
||||||
|
const proto = String(req?.headers?.['x-forwarded-proto'] || req?.protocol || 'http').split(',')[0].trim();
|
||||||
|
const host = String(req?.headers?.['x-forwarded-host'] || req?.headers?.host || '').split(',')[0].trim();
|
||||||
|
if (!host) return pathname;
|
||||||
|
return `${proto}://${host}${pathname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStationUpdateSummary(req) {
|
||||||
|
return {
|
||||||
|
localVersion: getLocalStationScriptVersion(),
|
||||||
|
targetVersion: stationUpdate.version || '',
|
||||||
|
releaseId: stationUpdate.releaseId || '',
|
||||||
|
publishedAt: Number(stationUpdate.publishedAt || 0),
|
||||||
|
downloadPath: STATION_SCRIPT_DOWNLOAD_PATH,
|
||||||
|
downloadUrl: buildAbsoluteUrl(req, STATION_SCRIPT_DOWNLOAD_PATH)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeSendStationUpdate(ws, stationName, stationVersion) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
||||||
|
if (!stationUpdate.releaseId || !stationUpdate.version) return false;
|
||||||
|
if (String(stationVersion || '').trim() === String(stationUpdate.version || '').trim()) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'station_update',
|
||||||
|
version: stationUpdate.version,
|
||||||
|
releaseId: stationUpdate.releaseId,
|
||||||
|
publishedAt: Number(stationUpdate.publishedAt || 0),
|
||||||
|
downloadPath: STATION_SCRIPT_DOWNLOAD_PATH
|
||||||
|
}));
|
||||||
|
log('INFO', `Queued station update for "${stationName}" -> ${stationUpdate.version}`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log('ERROR', `Failed to push station update to "${stationName}": ${e.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishStationUpdate(req) {
|
||||||
|
const version = getLocalStationScriptVersion() || `build-${Date.now()}`;
|
||||||
|
const releaseId = String(Date.now());
|
||||||
|
stationUpdate = {
|
||||||
|
version,
|
||||||
|
releaseId,
|
||||||
|
publishedAt: Date.now()
|
||||||
|
};
|
||||||
|
saveData();
|
||||||
|
|
||||||
|
let onlineStations = 0;
|
||||||
|
let notifiedStations = 0;
|
||||||
|
for (const [name, station] of Object.entries(stations)) {
|
||||||
|
const ws = station?.ws;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) continue;
|
||||||
|
onlineStations++;
|
||||||
|
if (maybeSendStationUpdate(ws, name, station.version)) {
|
||||||
|
notifiedStations++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = getStationUpdateSummary(req);
|
||||||
|
broadcastUpdate();
|
||||||
|
broadcastToConsole({
|
||||||
|
type: 'event',
|
||||||
|
title: '站点更新已发布',
|
||||||
|
message: `目标版本 ${summary.targetVersion || version},已通知 ${notifiedStations}/${onlineStations} 台在线主机。`
|
||||||
|
});
|
||||||
|
log('INFO', `Station update published: ${version} (${notifiedStations}/${onlineStations} online stations notified)`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
onlineStations,
|
||||||
|
notifiedStations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getInterruptModeForPriority(priority) {
|
function getInterruptModeForPriority(priority) {
|
||||||
if (priority >= 2) return 'all';
|
if (priority >= 2) return 'all';
|
||||||
if (priority >= 1) return 'ifLower';
|
if (priority >= 1) return 'ifLower';
|
||||||
@@ -281,6 +378,7 @@ function getStationGroups() {
|
|||||||
code,
|
code,
|
||||||
platform,
|
platform,
|
||||||
lastSeen: st.lastSeen || 0,
|
lastSeen: st.lastSeen || 0,
|
||||||
|
version: st.version || '',
|
||||||
source: 'cc',
|
source: 'cc',
|
||||||
tags: ['CC'],
|
tags: ['CC'],
|
||||||
memberNames: [st.name],
|
memberNames: [st.name],
|
||||||
@@ -295,6 +393,9 @@ function getStationGroups() {
|
|||||||
group.memberCount = group.memberNames.length;
|
group.memberCount = group.memberNames.length;
|
||||||
if ((st.lastSeen || 0) > (group.lastSeen || 0)) {
|
if ((st.lastSeen || 0) > (group.lastSeen || 0)) {
|
||||||
group.lastSeen = st.lastSeen || 0;
|
group.lastSeen = st.lastSeen || 0;
|
||||||
|
group.version = st.version || group.version || '';
|
||||||
|
} else if (!group.version && st.version) {
|
||||||
|
group.version = st.version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,6 +926,13 @@ function loadData() {
|
|||||||
if (data.activeTrains) activeTrains = data.activeTrains;
|
if (data.activeTrains) activeTrains = data.activeTrains;
|
||||||
if (data.audioVersion) audioVersion = String(data.audioVersion);
|
if (data.audioVersion) audioVersion = String(data.audioVersion);
|
||||||
if (Array.isArray(data.versions)) versions = data.versions;
|
if (Array.isArray(data.versions)) versions = data.versions;
|
||||||
|
if (data.stationUpdate && typeof data.stationUpdate === 'object') {
|
||||||
|
stationUpdate = {
|
||||||
|
version: String(data.stationUpdate.version || ''),
|
||||||
|
releaseId: String(data.stationUpdate.releaseId || ''),
|
||||||
|
publishedAt: Number(data.stationUpdate.publishedAt || 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
refreshStationIndex();
|
refreshStationIndex();
|
||||||
log('INFO', 'Data loaded from disk');
|
log('INFO', 'Data loaded from disk');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -846,7 +954,8 @@ function saveData() {
|
|||||||
stations: stationsToSave,
|
stations: stationsToSave,
|
||||||
activeTrains,
|
activeTrains,
|
||||||
audioVersion,
|
audioVersion,
|
||||||
versions
|
versions,
|
||||||
|
stationUpdate
|
||||||
};
|
};
|
||||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1672,9 +1781,10 @@ function handleMessage(ws, data) {
|
|||||||
// Send initial state
|
// Send initial state
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'init_state',
|
type: 'init_state',
|
||||||
stations,
|
stations: getMergedStationList(),
|
||||||
activeTrains,
|
activeTrains,
|
||||||
logs: stationLogs
|
logs: stationLogs,
|
||||||
|
stationUpdate: getStationUpdateSummary()
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1733,6 +1843,7 @@ function handleHeartbeat(ws, data) {
|
|||||||
};
|
};
|
||||||
normalizeStationRecord(stations[cleanName]);
|
normalizeStationRecord(stations[cleanName]);
|
||||||
log('INFO', `Station auto-registered via heartbeat: "${cleanName}"${data.stationVersion ? ` ${data.stationVersion}` : ''}`);
|
log('INFO', `Station auto-registered via heartbeat: "${cleanName}"${data.stationVersion ? ` ${data.stationVersion}` : ''}`);
|
||||||
|
maybeSendStationUpdate(ws, cleanName, data.stationVersion || stations[cleanName].version);
|
||||||
broadcastUpdate();
|
broadcastUpdate();
|
||||||
} else {
|
} else {
|
||||||
stations[cleanName].lastSeen = Date.now();
|
stations[cleanName].lastSeen = Date.now();
|
||||||
@@ -1745,6 +1856,7 @@ function handleHeartbeat(ws, data) {
|
|||||||
broadcastUpdate();
|
broadcastUpdate();
|
||||||
}
|
}
|
||||||
normalizeStationRecord(stations[cleanName]);
|
normalizeStationRecord(stations[cleanName]);
|
||||||
|
maybeSendStationUpdate(ws, cleanName, data.stationVersion || stations[cleanName].version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1771,6 +1883,7 @@ function handleStationRegister(ws, data) {
|
|||||||
stationName: cleanName
|
stationName: cleanName
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
maybeSendStationUpdate(ws, cleanName, stationVersion);
|
||||||
saveData();
|
saveData();
|
||||||
broadcastUpdate();
|
broadcastUpdate();
|
||||||
}
|
}
|
||||||
@@ -2066,11 +2179,26 @@ function broadcastUpdate() {
|
|||||||
const state = {
|
const state = {
|
||||||
type: 'state_update',
|
type: 'state_update',
|
||||||
stations: list,
|
stations: list,
|
||||||
activeTrains
|
activeTrains,
|
||||||
|
stationUpdate: getStationUpdateSummary()
|
||||||
};
|
};
|
||||||
broadcastToConsole(state);
|
broadcastToConsole(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get(STATION_SCRIPT_DOWNLOAD_PATH, (req, res) => {
|
||||||
|
if (!fs.existsSync(stationScriptPath)) {
|
||||||
|
return res.status(404).send('station_broadcast.lua not found');
|
||||||
|
}
|
||||||
|
return res.sendFile(stationScriptPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get(STATION_UPDATE_SCRIPT_DOWNLOAD_PATH, (req, res) => {
|
||||||
|
if (!fs.existsSync(stationUpdateScriptPath)) {
|
||||||
|
return res.status(404).send('update_station_broadcast.lua not found');
|
||||||
|
}
|
||||||
|
return res.sendFile(stationUpdateScriptPath);
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/streams', (req, res) => {
|
app.get('/api/streams', (req, res) => {
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -2193,6 +2321,21 @@ app.get('/api/version', (req, res) => {
|
|||||||
res.json({ version: audioVersion });
|
res.json({ version: audioVersion });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/station-update', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
...getStationUpdateSummary(req)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/station-updates/publish', (req, res) => {
|
||||||
|
const result = publishStationUpdate(req);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
...result
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/publish-version', (req, res) => {
|
app.post('/api/publish-version', (req, res) => {
|
||||||
const version = normalizeVersionString(req.body?.version) || String(Date.now());
|
const version = normalizeVersionString(req.body?.version) || String(Date.now());
|
||||||
const snap = snapshotFiles();
|
const snap = snapshotFiles();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local STATION_BROADCAST_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/station_broadcast.lua"
|
local STATION_BROADCAST_URL = "http://stabadmin.fse-media.group/downloads/station_broadcast.lua"
|
||||||
|
local UPDATE_SCRIPT_URL = "http://stabadmin.fse-media.group/downloads/update_station_broadcast.lua"
|
||||||
|
|
||||||
local function fetch(url)
|
local function fetch(url)
|
||||||
if not http then
|
if not http then
|
||||||
@@ -64,7 +65,8 @@ local function downloadTo(url, path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local targets = {
|
local targets = {
|
||||||
{ url = STATION_BROADCAST_URL, path = shell.resolve("/startup") }
|
{ url = STATION_BROADCAST_URL, path = shell.resolve("/startup") },
|
||||||
|
{ url = UPDATE_SCRIPT_URL, path = shell.resolve("/update_station_broadcast.lua") }
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, t in ipairs(targets) do
|
for _, t in ipairs(targets) do
|
||||||
@@ -76,4 +78,5 @@ for _, t in ipairs(targets) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
print("Install complete.")
|
print("Install complete.")
|
||||||
|
print("Update helper installed as /update_station_broadcast.lua")
|
||||||
print("Run 'reboot' to start StationBroadcast on boot.")
|
print("Run 'reboot' to start StationBroadcast on boot.")
|
||||||
|
|||||||
+180
-2
@@ -2,11 +2,12 @@
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- 1. Configuration & Constants
|
-- 1. Configuration & Constants
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
local APP_VERSION_TEXT = "v1.0.44"
|
local APP_VERSION_TEXT = "v1.0.45"
|
||||||
|
|
||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
||||||
API_URL = "http://stabadmin.fse-media.group",
|
API_URL = "http://stabadmin.fse-media.group",
|
||||||
|
STARTUP_PATH = "/startup",
|
||||||
SYNC_INTERVAL = 30,
|
SYNC_INTERVAL = 30,
|
||||||
AUDIO_BASE = "Audio/",
|
AUDIO_BASE = "Audio/",
|
||||||
AUDIO_STATION = "Audio/Station/",
|
AUDIO_STATION = "Audio/Station/",
|
||||||
@@ -55,7 +56,8 @@ local State = {
|
|||||||
currentPlaying = nil,
|
currentPlaying = nil,
|
||||||
currentPriority = -1,
|
currentPriority = -1,
|
||||||
interruptSeq = 0,
|
interruptSeq = 0,
|
||||||
tickRate = 20
|
tickRate = 20,
|
||||||
|
updateInProgress = false
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@@ -1813,6 +1815,162 @@ function Utils.getStationAudio(code, langPrefix)
|
|||||||
return langPrefix .. code
|
return langPrefix .. code
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local AutoUpdate = {}
|
||||||
|
|
||||||
|
function AutoUpdate.fetch(url)
|
||||||
|
if not http then
|
||||||
|
return nil, "HTTP API unavailable"
|
||||||
|
end
|
||||||
|
|
||||||
|
if http.checkURL then
|
||||||
|
local ok, err = http.checkURL(url)
|
||||||
|
if not ok then
|
||||||
|
return nil, err or "URL blocked"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local res, err = http.get(url)
|
||||||
|
if not res then
|
||||||
|
return nil, err or "request failed"
|
||||||
|
end
|
||||||
|
|
||||||
|
local body = res.readAll()
|
||||||
|
res.close()
|
||||||
|
|
||||||
|
if type(body) ~= "string" or #body == 0 then
|
||||||
|
return nil, "empty response"
|
||||||
|
end
|
||||||
|
|
||||||
|
return body
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.writeFile(path, content)
|
||||||
|
local dir = fs.getDir(path)
|
||||||
|
if dir and dir ~= "" and not fs.exists(dir) then
|
||||||
|
fs.makeDir(dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
local handle = fs.open(path, "w")
|
||||||
|
if not handle then
|
||||||
|
return nil, "cannot open " .. path
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.write(content)
|
||||||
|
handle.close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.getStartupPath()
|
||||||
|
if shell and shell.resolve then
|
||||||
|
return shell.resolve(CONFIG.STARTUP_PATH)
|
||||||
|
end
|
||||||
|
return CONFIG.STARTUP_PATH
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.getDownloadUrl(info)
|
||||||
|
if type(info) == "table" then
|
||||||
|
local direct = Utils.trim(info.downloadUrl)
|
||||||
|
if direct ~= "" then
|
||||||
|
return direct
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = Utils.trim(info.downloadPath)
|
||||||
|
if path ~= "" then
|
||||||
|
return CONFIG.API_URL .. path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return CONFIG.API_URL .. "/downloads/station_broadcast.lua"
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.fetchInfo()
|
||||||
|
local body, err = AutoUpdate.fetch(CONFIG.API_URL .. "/api/station-update")
|
||||||
|
if not body then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, data = pcall(textutils.unserializeJSON, body)
|
||||||
|
if not ok or type(data) ~= "table" then
|
||||||
|
return nil, "invalid update response"
|
||||||
|
end
|
||||||
|
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.getTargetVersion(info)
|
||||||
|
if type(info) ~= "table" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return Utils.trim(info.targetVersion or info.version)
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.needsUpdate(info)
|
||||||
|
local targetVersion = AutoUpdate.getTargetVersion(info)
|
||||||
|
return targetVersion ~= "" and targetVersion ~= APP_VERSION_TEXT
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.apply(info, reason)
|
||||||
|
if State.updateInProgress then
|
||||||
|
return false, "update already in progress"
|
||||||
|
end
|
||||||
|
|
||||||
|
local targetVersion = AutoUpdate.getTargetVersion(info)
|
||||||
|
if targetVersion == "" then
|
||||||
|
return false, "missing target version"
|
||||||
|
end
|
||||||
|
|
||||||
|
local downloadUrl = AutoUpdate.getDownloadUrl(info)
|
||||||
|
local startupPath = AutoUpdate.getStartupPath()
|
||||||
|
|
||||||
|
State.updateInProgress = true
|
||||||
|
Utils.log("WARN", "Applying update " .. targetVersion .. " via " .. tostring(reason or "manual"))
|
||||||
|
|
||||||
|
if Audio and Audio.stop then
|
||||||
|
pcall(Audio.stop, "all")
|
||||||
|
end
|
||||||
|
if State.ws then
|
||||||
|
pcall(State.ws.close)
|
||||||
|
State.ws = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local body, err = AutoUpdate.fetch(downloadUrl)
|
||||||
|
if not body then
|
||||||
|
State.updateInProgress = false
|
||||||
|
return false, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, writeErr = AutoUpdate.writeFile(startupPath, body)
|
||||||
|
if not ok then
|
||||||
|
State.updateInProgress = false
|
||||||
|
return false, writeErr
|
||||||
|
end
|
||||||
|
|
||||||
|
Utils.log("SUCCESS", "Updated startup to " .. targetVersion)
|
||||||
|
Utils.log("WARN", "Rebooting to finish update")
|
||||||
|
os.sleep(1)
|
||||||
|
os.reboot()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoUpdate.checkOnBoot()
|
||||||
|
local info, err = AutoUpdate.fetchInfo()
|
||||||
|
if not info then
|
||||||
|
Utils.log("WARN", "Update check skipped: " .. tostring(err))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not AutoUpdate.needsUpdate(info) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
Utils.log("INFO", "Found published update " .. AutoUpdate.getTargetVersion(info) .. " on boot")
|
||||||
|
local ok, applyErr = AutoUpdate.apply(info, "boot")
|
||||||
|
if not ok then
|
||||||
|
Utils.log("ERROR", "Boot update failed: " .. tostring(applyErr))
|
||||||
|
end
|
||||||
|
return ok
|
||||||
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- 6. Network System
|
-- 6. Network System
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@@ -1990,6 +2148,25 @@ function Network.messageLoop()
|
|||||||
local flushBelow = data.flushBelow
|
local flushBelow = data.flushBelow
|
||||||
Audio.stop(mode, flushBelow)
|
Audio.stop(mode, flushBelow)
|
||||||
end
|
end
|
||||||
|
elseif data.type == "station_update" then
|
||||||
|
local targetVersion = AutoUpdate.getTargetVersion(data)
|
||||||
|
if targetVersion == "" then
|
||||||
|
Utils.log("WARN", "Ignored station update without version")
|
||||||
|
elseif targetVersion == APP_VERSION_TEXT then
|
||||||
|
Utils.log("INFO", "Received update notice for current version " .. targetVersion)
|
||||||
|
else
|
||||||
|
Utils.log("WARN", "Received published update " .. targetVersion)
|
||||||
|
local okUpdate, updateErr = AutoUpdate.apply({
|
||||||
|
targetVersion = targetVersion,
|
||||||
|
version = targetVersion,
|
||||||
|
releaseId = data.releaseId,
|
||||||
|
downloadPath = data.downloadPath,
|
||||||
|
downloadUrl = data.downloadUrl
|
||||||
|
}, "publish")
|
||||||
|
if not okUpdate then
|
||||||
|
Utils.log("ERROR", "Station update failed: " .. tostring(updateErr))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if State.audioRx then
|
if State.audioRx then
|
||||||
@@ -2842,6 +3019,7 @@ end
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- 9. Bootstrap
|
-- 9. Bootstrap
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
AutoUpdate.checkOnBoot()
|
||||||
Peripherals.init()
|
Peripherals.init()
|
||||||
Utils.ensureSlaveIdentityConfigured()
|
Utils.ensureSlaveIdentityConfigured()
|
||||||
Audio.loadStationList()
|
Audio.loadStationList()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
local STATION_BROADCAST_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/station_broadcast.lua"
|
local STATION_BROADCAST_URL = "http://stabadmin.fse-media.group/downloads/station_broadcast.lua"
|
||||||
|
|
||||||
local function fetch(url)
|
local function fetch(url)
|
||||||
if not http then
|
if not http then
|
||||||
|
|||||||
Reference in New Issue
Block a user