UNPKG

@pickstar-2002/video-fetch-mcp

Version:

基于 MCP 协议的多平台视频下载服务端

279 lines (244 loc) 7.92 kB
/** * REST API 服务器实现 */ import express, { Application, Request, Response, NextFunction } from 'express'; import cors from 'cors'; import helmet from 'helmet'; import { VideoDownloadRequest } from '../types.js'; import YtDlpService from './ytdlp-service.js'; import { logger } from '../utils/logger.js'; import config from '../config.js'; import Joi from 'joi'; export class RestApiServer { private app: Application; private ytdlpService: YtDlpService; constructor() { this.app = express(); this.ytdlpService = new YtDlpService(); this.setupMiddleware(); this.setupRoutes(); this.setupErrorHandling(); } /** * 设置中间件 */ private setupMiddleware(): void { // 安全头 if (config.api.enableSecurity) { this.app.use(helmet()); } // CORS if (config.api.enableCors) { this.app.use(cors({ origin: true, credentials: true, })); } // 解析 JSON this.app.use(express.json({ limit: config.api.bodyLimit })); this.app.use(express.urlencoded({ extended: true, limit: config.api.bodyLimit })); // 请求日志 this.app.use((req: Request, res: Response, next: NextFunction) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, userAgent: req.get('User-Agent'), body: req.method === 'POST' ? req.body : undefined }); next(); }); } /** * 设置路由 */ private setupRoutes(): void { const router = express.Router(); // 健康检查 router.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), version: config.mcp.version }); }); // 获取视频信息 router.post('/video/info', async (req: Request, res: Response, next: NextFunction) => { try { const schema = Joi.object({ url: Joi.string().uri().required() }); const { error, value } = schema.validate(req.body); if (error) { return res.status(400).json({ success: false, error: `参数验证失败: ${error.message}` }); } const videoInfo = await this.ytdlpService.getVideoInfo(value.url); return res.json({ success: true, data: videoInfo }); } catch (error) { return next(error); } }); // 开始下载视频 router.post('/video/download', async (req: Request, res: Response, next: NextFunction) => { try { const schema = Joi.object({ url: Joi.string().uri().required(), outputPath: Joi.string().required(), quality: Joi.string().valid('best', 'worst', 'bestvideo', 'bestaudio').default('best'), outputTemplate: Joi.string().default('%(title)s.%(ext)s'), extractAudio: Joi.boolean().default(false), audioFormat: Joi.string().valid('mp3', 'aac', 'wav', 'flac').default('mp3'), downloadSubtitles: Joi.boolean().default(false), subtitleLangs: Joi.array().items(Joi.string()).default(['zh-CN', 'en']) }); const { error, value } = schema.validate(req.body); if (error) { return res.status(400).json({ success: false, error: `参数验证失败: ${error.message}` }); } const request: VideoDownloadRequest = value; const taskId = await this.ytdlpService.downloadVideo(request); return res.json({ success: true, data: { taskId, message: '下载任务已创建', request } }); } catch (error) { return next(error); } }); // 获取任务状态 router.get('/task/:taskId', async (req: Request, res: Response, next: NextFunction) => { try { const { taskId } = req.params; const task = this.ytdlpService.getTaskStatus(taskId); if (!task) { return res.status(404).json({ success: false, error: '任务不存在' }); } return res.json({ success: true, data: task }); } catch (error) { return next(error); } }); // 取消任务 router.delete('/task/:taskId', async (req: Request, res: Response, next: NextFunction) => { try { const { taskId } = req.params; const success = this.ytdlpService.cancelTask(taskId); res.json({ success, message: success ? '任务已取消' : '任务不存在或无法取消' }); } catch (error) { next(error); } }); // 列出所有任务 router.get('/tasks', async (req: Request, res: Response, next: NextFunction) => { try { const tasks = this.ytdlpService.getAllTasks(); res.json({ success: true, data: { tasks, count: tasks.length } }); } catch (error) { next(error); } }); // 获取支持的平台列表 router.get('/platforms', (req: Request, res: Response) => { res.json({ success: true, data: { platforms: config.supportedPlatforms, count: config.supportedPlatforms.length } }); }); // 应用路由 this.app.use(config.api.prefix, router); // 根路径重定向到 API 文档 this.app.get('/', (req: Request, res: Response) => { res.json({ name: config.mcp.name, version: config.mcp.version, description: config.mcp.description, endpoints: { health: `GET ${config.api.prefix}/health`, videoInfo: `POST ${config.api.prefix}/video/info`, downloadVideo: `POST ${config.api.prefix}/video/download`, getTask: `GET ${config.api.prefix}/task/:taskId`, cancelTask: `DELETE ${config.api.prefix}/task/:taskId`, listTasks: `GET ${config.api.prefix}/tasks`, platforms: `GET ${config.api.prefix}/platforms` } }); }); } /** * 设置错误处理 */ private setupErrorHandling(): void { // 404 处理 this.app.use((req: Request, res: Response) => { res.status(404).json({ success: false, error: '接口不存在', path: req.path }); }); // 全局错误处理 this.app.use((error: Error, req: Request, res: Response, next: NextFunction) => { logger.error('API 错误:', error); res.status(500).json({ success: false, error: error.message || '内部服务器错误', timestamp: new Date().toISOString() }); }); } /** * 启动服务器 */ async start(): Promise<void> { return new Promise((resolve) => { const server = this.app.listen(config.port, () => { logger.info(`REST API 服务器已启动,端口: ${config.port}`); logger.info(`API 文档: http://localhost:${config.port}`); logger.info(`健康检查: http://localhost:${config.port}${config.api.prefix}/health`); // 定期清理过期任务 setInterval(() => { this.ytdlpService.cleanupExpiredTasks(); }, 60000); // 每分钟清理一次 resolve(); }); // 优雅关闭 process.on('SIGINT', () => { logger.info('正在关闭服务器...'); server.close(() => { logger.info('服务器已关闭'); process.exit(0); }); }); }); } } export default RestApiServer;