const express = require("express"); const { createDetailedHealthRouter, createHealthRouter, } = require("./health/routes"); const { createAssetRouter } = require("./assets/routes"); const { createMediaRouter } = require("./assets/media-routes"); const { createAssetService } = require("./assets/service"); const { createPlaybackService } = require("./playback"); const { createPlaybackRouter } = require("./playback/routes"); const { createPlaylistRouter, createPlaylistService, } = require("./playlists"); const { createSettingsRouter } = require("./settings/routes"); const { createSettingsService } = require("./settings/service"); function safeNotifier(notifier, logger, failureMessage) { const warn = (error) => { try { logger?.warn?.(failureMessage, error); } catch { // A best-effort notification or warning must not undo a database commit. } }; return (event) => { try { const result = notifier(event); result?.catch?.(warn); } catch (error) { warn(error); } }; } function createApp({ config, database, imageProcessor, lifecycle, logger = console, migrations, now = Date.now, onPlaylistsChanged = () => {}, playbackService: suppliedPlaybackService, storage, }) { const app = express(); const notifyPlaylistsChanged = safeNotifier( onPlaylistsChanged, logger, "Playlist invalidation notification failed", ); const playbackService = suppliedPlaybackService || createPlaybackService({ database, now, onActivationChanged: notifyPlaylistsChanged, }); if (!suppliedPlaybackService) { playbackService.initialize(); } const reconcilePlaybackDomain = (change) => playbackService.domainChanged(change); const reconcileBeforeDomainChange = () => playbackService.reconcile("before_domain_change"); const notifyPlaylistMutation = (change) => { reconcilePlaybackDomain(change); const publicChange = { ...change }; delete publicChange.invalidBoundaryChanged; notifyPlaylistsChanged(publicChange); }; const settingsService = createSettingsService(database); const assetService = createAssetService({ database, imageProcessor, storage, }); const playlistService = createPlaylistService({ database, now, onBeforeChange: reconcileBeforeDomainChange, onChanged: notifyPlaylistMutation, }); app.locals.playbackService = playbackService; app.disable("x-powered-by"); app.set("trust proxy", config.trustProxy); app.use(express.json({ limit: "1mb" })); app.get("/api", (request, response) => { response.json({ name: "Announcements API", version: 2 }); }); app.use( "/health", createHealthRouter({ database, lifecycle, migrations, storage }), ); const detailedHealthRouterOptions = { config, database, lifecycle, migrations, storage, }; app.use( "/api/health", createDetailedHealthRouter(detailedHealthRouterOptions), ); app.use( "/api/status", createDetailedHealthRouter(detailedHealthRouterOptions), ); app.use( "/api/settings", createSettingsRouter(settingsService, { onBeforePlaybackDomainChange: reconcileBeforeDomainChange, onPlaylistsChanged: notifyPlaylistsChanged, onPlaybackDomainChanged: reconcilePlaybackDomain, }), ); app.use( "/api/assets", createAssetRouter({ assetService, onBeforePlaybackDomainChange: reconcileBeforeDomainChange, onPlaylistsChanged: notifyPlaylistsChanged, onPlaybackDomainChanged: reconcilePlaybackDomain, processingConfig: config.processing, }), ); app.use( "/api/playlists", createPlaylistRouter(playlistService, { playbackService, }), ); app.use("/api/playback", createPlaybackRouter(playbackService)); app.use("/media", createMediaRouter(assetService)); app.use((request, response) => { response.status(404).json({ error: { code: "NOT_FOUND", message: "Route not found", }, }); }); app.use((error, request, response, next) => { if (response.headersSent) { next(error); return; } if (error instanceof SyntaxError && error.status === 400 && "body" in error) { response.status(400).json({ error: { code: "INVALID_JSON", message: "Request body must contain valid JSON", }, }); return; } const statusCode = error.statusCode || 500; response.status(statusCode).json({ error: { code: error.code || "INTERNAL_ERROR", message: statusCode >= 500 ? "An internal server error occurred" : error.message, ...(error.details ? { details: error.details } : {}), }, }); }); return app; } module.exports = { createApp, };