Initial Arcana FX soundboard
This commit is contained in:
125
src/server.js
Normal file
125
src/server.js
Normal file
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('node:path');
|
||||
const express = require('express');
|
||||
|
||||
const { loadConfig } = require('./config');
|
||||
const { FilestashClient } = require('./filestash');
|
||||
const { EffectsService } = require('./effects');
|
||||
const { HomeAssistantClient } = require('./homeassistant');
|
||||
const { createApiRouter } = require('./routes');
|
||||
|
||||
function buildApp(config) {
|
||||
const filestash = new FilestashClient({
|
||||
baseUrl: config.filestash.baseUrl,
|
||||
shareId: config.filestash.shareId,
|
||||
});
|
||||
|
||||
const effects = new EffectsService({
|
||||
client: filestash,
|
||||
extensions: config.audio.extensions,
|
||||
ignoreNames: config.audio.ignoreNames,
|
||||
cacheTtlSeconds: config.audio.cacheTtlSeconds,
|
||||
maxDepth: config.audio.maxDepth,
|
||||
});
|
||||
|
||||
let ha = null;
|
||||
if (config.env.haUrl && config.env.haToken) {
|
||||
ha = new HomeAssistantClient({
|
||||
baseUrl: config.env.haUrl,
|
||||
token: config.env.haToken,
|
||||
allowedEntity: config.homeAssistant.allowedMediaPlayer,
|
||||
timeoutMs: config.homeAssistant.requestTimeoutMs,
|
||||
});
|
||||
} else {
|
||||
// Stub that fails closed so /api/play returns a clear error
|
||||
// instead of a server crash when env is incomplete.
|
||||
ha = {
|
||||
async playMedia() {
|
||||
const err = new Error('HA_URL and HA_TOKEN must be set to play media');
|
||||
err.status = 503;
|
||||
throw err;
|
||||
},
|
||||
async stop() {
|
||||
const err = new Error('HA_URL and HA_TOKEN must be set to stop playback');
|
||||
err.status = 503;
|
||||
throw err;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Lightweight CORS: only when ALLOWED_ORIGINS is set.
|
||||
if (config.env.allowedOrigins.length) {
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
if (origin && config.env.allowedOrigins.includes(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Vary', 'Origin');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
}
|
||||
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(createApiRouter({ config, effects, filestash, ha }));
|
||||
|
||||
const publicDir = path.join(__dirname, '..', 'public');
|
||||
app.use(express.static(publicDir, { fallthrough: true, index: 'index.html' }));
|
||||
|
||||
// JSON error handler — must keep 4 args for Express to recognize it.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.use((err, _req, res, _next) => {
|
||||
const status = err.status || 500;
|
||||
if (status >= 500) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[error]', err);
|
||||
}
|
||||
res.status(status).json({ error: err.message || 'internal error' });
|
||||
});
|
||||
|
||||
return { app, effects, filestash };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const config = loadConfig();
|
||||
const { app, effects } = buildApp(config);
|
||||
|
||||
const server = app.listen(config.env.port, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Arcana FX listening on :${config.env.port}`);
|
||||
if (!config.env.publicBaseUrl) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[warn] PUBLIC_BASE_URL is empty — /api/play will reject until it is set.');
|
||||
}
|
||||
if (!config.env.haToken) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[warn] HA_TOKEN is empty — playback endpoints will return 503.');
|
||||
}
|
||||
});
|
||||
|
||||
// Warm the cache in the background; ignore failures, they will surface on /api/effects.
|
||||
effects.list().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[warn] initial effects scan failed:', err.message);
|
||||
});
|
||||
|
||||
for (const sig of ['SIGINT', 'SIGTERM']) {
|
||||
process.on(sig, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Received ${sig}, shutting down.`);
|
||||
server.close(() => process.exit(0));
|
||||
setTimeout(() => process.exit(1), 5000).unref();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { buildApp };
|
||||
Reference in New Issue
Block a user