From 94e497b7a67b6b8a3cebbbc3c452664984997969 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Fri, 15 May 2026 17:43:53 +0700 Subject: [PATCH] feat: expose media playback routes Co-Authored-By: Claude Opus 4.7 --- src/routes/mediaRoutes.ts | 51 ++++++++++++++++++++++++ tests/routes/mediaRoutes.test.ts | 68 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/routes/mediaRoutes.ts create mode 100644 tests/routes/mediaRoutes.test.ts diff --git a/src/routes/mediaRoutes.ts b/src/routes/mediaRoutes.ts new file mode 100644 index 0000000..040628b --- /dev/null +++ b/src/routes/mediaRoutes.ts @@ -0,0 +1,51 @@ +import type { Router } from "express"; +import express from "express"; +import { AppError } from "../errors"; +import type { MediaController } from "../media/mediaController"; + +export type MediaRouteController = Pick< + MediaController, + "getState" | "queue" | "skip" | "stop" +>; + +export function createMediaRoutes(controller: MediaRouteController): Router { + const router = express.Router(); + + router.get("/media/status", (_req, res, next) => { + try { + res.json(controller.getState()); + } catch (error) { + next(error); + } + }); + + router.post("/media/queue", async (req, res, next) => { + try { + const { source } = req.body as { source?: string }; + if (!source) { + throw new AppError("Media source is required", "MISSING_MEDIA_SOURCE", 400); + } + res.json(await controller.queue(source)); + } catch (error) { + next(error); + } + }); + + router.post("/media/skip", async (_req, res, next) => { + try { + res.json(await controller.skip()); + } catch (error) { + next(error); + } + }); + + router.post("/media/stop", async (_req, res, next) => { + try { + res.json(await controller.stop()); + } catch (error) { + next(error); + } + }); + + return router; +} \ No newline at end of file diff --git a/tests/routes/mediaRoutes.test.ts b/tests/routes/mediaRoutes.test.ts new file mode 100644 index 0000000..3a1095f --- /dev/null +++ b/tests/routes/mediaRoutes.test.ts @@ -0,0 +1,68 @@ +import type { Request, Response } from "express"; +import { describe, expect, it, vi } from "vitest"; +import { createMediaRoutes } from "../../src/routes/mediaRoutes"; + +function getHandler(router: ReturnType, path: string, method: string) { + const layer = router.stack.find((item) => item.route?.path === path); + return layer?.route?.stack.find((item) => item.method === method)?.handle; +} + +describe("createMediaRoutes", () => { + it("returns media status", async () => { + const controller = { + getState: vi.fn(() => ({ playing: false, current: null, queue: [] })), + queue: vi.fn(), + skip: vi.fn(), + stop: vi.fn(), + }; + const handler = getHandler(createMediaRoutes(controller), "/media/status", "get"); + const json = vi.fn(); + + await handler?.({} as Request, { json } as unknown as Response, vi.fn()); + + expect(json).toHaveBeenCalledWith({ playing: false, current: null, queue: [] }); + }); + + it("queues a source", async () => { + const state = { playing: true, current: null, queue: [] }; + const controller = { + getState: vi.fn(), + queue: vi.fn(async () => state), + skip: vi.fn(), + stop: vi.fn(), + }; + const handler = getHandler(createMediaRoutes(controller), "/media/queue", "post"); + const json = vi.fn(); + + await handler?.( + { body: { source: "https://example.com/song.mp3" } } as Request, + { json } as unknown as Response, + vi.fn(), + ); + + expect(controller.queue).toHaveBeenCalledWith("https://example.com/song.mp3"); + expect(json).toHaveBeenCalledWith(state); + }); + + it("passes missing source errors to Express", async () => { + const controller = { + getState: vi.fn(), + queue: vi.fn(), + skip: vi.fn(), + stop: vi.fn(), + }; + const handler = getHandler(createMediaRoutes(controller), "/media/queue", "post"); + const next = vi.fn(); + + await handler?.( + { body: {} } as Request, + { json: vi.fn() } as unknown as Response, + next, + ); + + expect(next.mock.calls[0][0]).toMatchObject({ + code: "MISSING_MEDIA_SOURCE", + statusCode: 400, + }); + }); +});