Files
radio-explorer/server/start.js
2026-05-27 12:54:56 +02:00

111 lines
4.4 KiB
JavaScript

// Bootstraps the HTTP API + WebSocket hub + static assets.
//
// Exposed as a function so the Electron main process can boot the server
// in-process (sharing the event loop, no IPC). `server/index.js` calls
// this for plain `node server/index.js` CLI use.
import 'dotenv/config';
import express from 'express';
import { createServer } from 'node:http';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
import { initDb } from './db/index.js';
import { authMiddleware, ensureBootstrapAdmin, ensureMainUser, ensureBootstrapApiKey } from './auth.js';
import { applySeedIfEmpty } from './sources/seed.js';
import { scheduleHealthCheck } from './streams/checker.js';
import { attachWs } from './ws.js';
import { ensureImageDirs, getImageRoot } from './media/images.js';
import { router as authRoutes } from './routes/auth.js';
import { router as stationRoutes } from './routes/stations.js';
import { router as meRoutes } from './routes/me.js';
import { router as adminRoutes } from './routes/admin.js';
import { router as v1Routes } from './routes/v1.js';
import { router as roomRoutes } from './routes/rooms.js';
import { router as userRoutes } from './routes/users.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
export async function startServer(opts = {}) {
const port = Number(opts.port ?? process.env.PORT ?? 4173);
const host = opts.host ?? process.env.HOST ?? '0.0.0.0';
const dbPath = opts.dbPath ?? process.env.DB_PATH ?? './data/db/oradio.sqlite';
initDb(dbPath);
ensureBootstrapAdmin({
username: process.env.ADMIN_BOOTSTRAP_USER,
password: process.env.ADMIN_BOOTSTRAP_PASSWORD
});
ensureMainUser(process.env.MAIN_USER || 'morphix');
ensureBootstrapApiKey(process.env.ORADIO_API_KEY);
const seedResult = applySeedIfEmpty();
console.log('[seed]', seedResult);
ensureImageDirs();
const app = express();
app.use(express.json({ limit: '512kb' }));
app.use(authMiddleware);
app.use('/api/auth', authRoutes);
app.use('/api/stations', stationRoutes);
app.use('/api/me', meRoutes);
app.use('/api/users', userRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/rooms', roomRoutes);
app.use('/api/v1', v1Routes);
// Locally-cached cover art and other media live under the configured image
// root and are served unauthenticated on the LAN. Long cache OK — file names
// include the station id and we rewrite the file on update.
app.use('/media', express.static(getImageRoot(), {
maxAge: '1h',
fallthrough: false,
setHeaders(res) { res.setHeader('Cache-Control', 'public, max-age=3600'); }
}));
// Static assets (built by Vite). In dev these don't exist; Vite serves them on :5173.
const publicDir = opts.publicDir ?? resolve(__dirname, 'public');
const sendHtml = (file) => (_req, res) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile(resolve(publicDir, file));
};
if (existsSync(publicDir)) {
app.use(express.static(publicDir, {
setHeaders(res, filePath) {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
} else if (filePath.includes(`${publicDir}\\assets\\`) || filePath.includes(`${publicDir}/assets/`)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}
}));
app.get('/admin', sendHtml('admin/index.html'));
app.get('/docs', sendHtml('docs/index.html'));
app.get('/master', sendHtml('master/index.html'));
app.get('*', sendHtml('index.html'));
}
app.use((err, _req, res, _next) => {
console.error(err);
res.status(500).json({ error: String(err.message || err) });
});
const server = createServer(app);
attachWs(server);
scheduleHealthCheck(process.env.STREAM_CHECK_CRON);
await new Promise((resolveListen) => server.listen(port, host, resolveListen));
const addr = server.address();
const actualPort = (addr && typeof addr === 'object') ? addr.port : port;
console.log(`[oradio] api+ws on http://${host}:${actualPort}`);
return {
server,
port: actualPort,
host,
stop: () => new Promise((res) => server.close(() => res()))
};
}