feat: expose media playback routes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
51
src/routes/mediaRoutes.ts
Normal file
51
src/routes/mediaRoutes.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
68
tests/routes/mediaRoutes.test.ts
Normal file
68
tests/routes/mediaRoutes.test.ts
Normal file
@@ -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<typeof createMediaRoutes>, 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user