123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
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<void> {
|
|
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;
|
|
}
|
|
}
|