import { Application, Router, send } from '@oak/oak'; import type { AppConfig } from '../shared/config.ts'; import { logger } from '../shared/logger.ts'; import { createTrackRoutes } from './routes/trackRoutes.ts'; import { createAudioRoutes } from './routes/audioRoutes.ts'; import type { TrackService } from '../application/mod.ts'; import type { AudioStreamingService, CoverArtService } from '../infrastructure/mod.ts'; /** * HTTP server using Oak */ export class HttpServer { private readonly app: Application; constructor( private readonly config: AppConfig, private readonly trackService: TrackService, private readonly audioStreaming: AudioStreamingService, private readonly coverArt: CoverArtService, ) { this.app = new Application(); this.setupMiddleware(); this.setupRoutes(); } /** * Setup middleware */ private setupMiddleware(): void { // Error handling this.app.use(async (ctx, next) => { try { await next(); } catch (error) { logger.error(`Request error: ${error}`); ctx.response.status = 500; ctx.response.body = { error: 'Internal server error' }; } }); // Logging this.app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; logger.info(`${ctx.request.method} ${ctx.request.url.pathname} - ${ctx.response.status} - ${ms}ms`); }); // CORS this.app.use(async (ctx, next) => { ctx.response.headers.set('Access-Control-Allow-Origin', this.config.corsOrigin); ctx.response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); ctx.response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Range'); ctx.response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, Accept-Ranges'); if (ctx.request.method === 'OPTIONS') { ctx.response.status = 204; return; } await next(); }); } /** * Setup routes */ private setupRoutes(): void { const router = new Router(); // API routes const trackRoutes = createTrackRoutes(this.trackService); const audioRoutes = createAudioRoutes(this.audioStreaming, this.coverArt, this.config); router.use(trackRoutes.routes(), trackRoutes.allowedMethods()); router.use(audioRoutes.routes(), audioRoutes.allowedMethods()); this.app.use(router.routes()); this.app.use(router.allowedMethods()); // Static file serving (public directory) this.app.use(async (ctx) => { try { await send(ctx, ctx.request.url.pathname, { root: this.config.publicDir, index: 'index.html', }); } catch { // If file not found, serve index.html for SPA routing try { await send(ctx, '/index.html', { root: this.config.publicDir, }); } catch (error) { ctx.response.status = 404; ctx.response.body = 'Not found'; } } }); } /** * Start the HTTP server */ async listen(): Promise { const { host, port } = this.config; logger.info(`Starting HTTP server on http://${host}:${port}`); await this.app.listen({ hostname: host, port, }); } /** * Get the underlying Oak application */ getApp(): Application { return this.app; } }