Compare commits

..
2 Commits
Author SHA1 Message Date
Henry_Du aef63ca444 fix: install updater alongside startup script
Download the update helper during install and expose it from the cloud server so new hosts can run manual updates immediately.
2026-08-02 21:20:28 +08:00
Henry_Du 10d15dfddc feat: add station auto update publishing
Publish the latest station script from the console so online hosts download and reboot immediately while offline hosts update on their next load.
2026-08-02 20:04:55 +08:00
5 changed files with 396 additions and 22 deletions
+62 -12
View File
@@ -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
View File
@@ -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();
+5 -2
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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