From ff2792d403ae8493f85e1f256c7b0fb2951f3703 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Fri, 15 May 2026 18:04:39 +0700 Subject: [PATCH] style: format media music implementation --- src/media/mediaController.ts | 18 +++++---- src/media/mediaQueue.ts | 2 +- src/media/mediaTypes.ts | 2 +- src/media/musicPlayer.ts | 2 +- src/moderation/broadcaster.ts | 2 +- src/moderation/types.ts | 2 +- src/routes/mediaRoutes.ts | 8 +++- src/webserver.ts | 9 ++++- tests/media/mediaController.test.ts | 27 ++++++++++--- tests/media/mediaQueue.test.ts | 29 +++++++++++--- tests/media/mediaResolver.test.ts | 18 ++++++--- tests/media/musicPlayer.test.ts | 59 ++++++++++++++++++----------- tests/routes/mediaRoutes.test.ts | 34 ++++++++++++++--- 13 files changed, 150 insertions(+), 62 deletions(-) diff --git a/src/media/mediaController.ts b/src/media/mediaController.ts index 96f72cd..8efb236 100644 --- a/src/media/mediaController.ts +++ b/src/media/mediaController.ts @@ -2,13 +2,13 @@ import { AppError } from "../errors"; import { discordPlayer } from "../player"; import { MediaQueue } from "./mediaQueue"; import { resolveMediaSource } from "./mediaResolver"; -import { createMusicPlayer } from "./musicPlayer"; import type { MediaState, MusicPlayback, MusicPlayer, ResolvedMediaSource, } from "./mediaTypes"; +import { createMusicPlayer } from "./musicPlayer"; export interface MediaControllerDependencies { isVoiceConnected?: () => boolean; @@ -39,9 +39,9 @@ export class MediaController { async queue(source: string): Promise { this.assertCanStart(); - const resolved = await (this.dependencies.resolveMediaSource ?? resolveMediaSource)( - source, - ); + const resolved = await ( + this.dependencies.resolveMediaSource ?? resolveMediaSource + )(source); this.queueStore.add(resolved); this.startNextIfIdle(); return this.emitState(); @@ -49,7 +49,11 @@ export class MediaController { async skip(): Promise { if (this.skipInProgress) { - throw new AppError("Skip already in progress", "MEDIA_SKIP_IN_PROGRESS", 409); + throw new AppError( + "Skip already in progress", + "MEDIA_SKIP_IN_PROGRESS", + 409, + ); } this.skipInProgress = true; @@ -74,8 +78,8 @@ export class MediaController { } private assertCanStart(): void { - const isVoiceConnected = this.dependencies.isVoiceConnected ?? - (() => discordPlayer.isConnected()); + const isVoiceConnected = + this.dependencies.isVoiceConnected ?? (() => discordPlayer.isConnected()); if (!isVoiceConnected()) { throw new AppError( "Connect to a voice channel before playing media", diff --git a/src/media/mediaQueue.ts b/src/media/mediaQueue.ts index d412e5b..a98b265 100644 --- a/src/media/mediaQueue.ts +++ b/src/media/mediaQueue.ts @@ -56,4 +56,4 @@ export class MediaQueue { queue: this.items.map((item) => ({ ...item })), }; } -} \ No newline at end of file +} diff --git a/src/media/mediaTypes.ts b/src/media/mediaTypes.ts index c6a1f56..c9ce5e5 100644 --- a/src/media/mediaTypes.ts +++ b/src/media/mediaTypes.ts @@ -37,4 +37,4 @@ export interface DiscordAudioPlayer { isConnected(): boolean; playStream(stream: Readable): void; stop(): void; -} \ No newline at end of file +} diff --git a/src/media/musicPlayer.ts b/src/media/musicPlayer.ts index ce8e40b..e03ce01 100644 --- a/src/media/musicPlayer.ts +++ b/src/media/musicPlayer.ts @@ -1,5 +1,5 @@ -import { spawn as nodeSpawn } from "node:child_process"; import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn as nodeSpawn } from "node:child_process"; import { discordPlayer } from "../player"; import type { DiscordAudioPlayer, diff --git a/src/moderation/broadcaster.ts b/src/moderation/broadcaster.ts index f49d800..4dec06a 100644 --- a/src/moderation/broadcaster.ts +++ b/src/moderation/broadcaster.ts @@ -79,4 +79,4 @@ export function createBroadcaster() { }; } -export type ModerationBroadcaster = ReturnType; \ No newline at end of file +export type ModerationBroadcaster = ReturnType; diff --git a/src/moderation/types.ts b/src/moderation/types.ts index baa627b..a6a6cbd 100644 --- a/src/moderation/types.ts +++ b/src/moderation/types.ts @@ -128,4 +128,4 @@ export interface AnalysisQueueStatus { queuedConversations: number; activeRequests: number; lastError: string | null; -} \ No newline at end of file +} diff --git a/src/routes/mediaRoutes.ts b/src/routes/mediaRoutes.ts index 040628b..d1e79a1 100644 --- a/src/routes/mediaRoutes.ts +++ b/src/routes/mediaRoutes.ts @@ -23,7 +23,11 @@ export function createMediaRoutes(controller: MediaRouteController): Router { try { const { source } = req.body as { source?: string }; if (!source) { - throw new AppError("Media source is required", "MISSING_MEDIA_SOURCE", 400); + throw new AppError( + "Media source is required", + "MISSING_MEDIA_SOURCE", + 400, + ); } res.json(await controller.queue(source)); } catch (error) { @@ -48,4 +52,4 @@ export function createMediaRoutes(controller: MediaRouteController): Router { }); return router; -} \ No newline at end of file +} diff --git a/src/webserver.ts b/src/webserver.ts index d1e3790..f398dd0 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -8,6 +8,7 @@ import * as prism from "prism-media"; import { WebSocketServer } from "ws"; import { AppError } from "./errors"; import { createChildLogger, logger } from "./logger"; +import { MediaController } from "./media/mediaController"; import { getMetrics, uptimeGauge } from "./metrics"; import { createBroadcaster } from "./moderation/broadcaster"; import type { ModerationBroadcaster } from "./moderation/types"; @@ -19,7 +20,6 @@ import { createMessageRoutes } from "./routes/messageRoutes"; import { createSyncRoutes } from "./routes/syncRoutes"; import { createUIStateRoutes } from "./routes/uiStateRoutes"; import { createVoiceRoutes } from "./routes/voiceRoutes"; -import { MediaController } from "./media/mediaController"; import type { VoiceController } from "./voiceController"; const wsLogger = createChildLogger("webserver"); @@ -378,7 +378,12 @@ export async function startWebserver( }), ); ws.send(JSON.stringify({ type: "ui_state", state: getSharedUIState() })); - ws.send(JSON.stringify({ type: "media_state", state: mediaController.getState() })); + ws.send( + JSON.stringify({ + type: "media_state", + state: mediaController.getState(), + }), + ); ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => { if (!Buffer.isBuffer(data)) return; diff --git a/tests/media/mediaController.test.ts b/tests/media/mediaController.test.ts index 9d62186..de41a4d 100644 --- a/tests/media/mediaController.test.ts +++ b/tests/media/mediaController.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it, vi } from "vitest"; import { AppError } from "../../src/errors"; import { MediaController } from "../../src/media/mediaController"; -import type { MusicPlayback, MusicPlayer, ResolvedMediaSource } from "../../src/media/mediaTypes"; +import type { + MusicPlayback, + MusicPlayer, + ResolvedMediaSource, +} from "../../src/media/mediaTypes"; function deferred() { let resolve!: () => void; @@ -26,7 +30,9 @@ describe("MediaController", () => { musicPlayer: { play: vi.fn() }, }); - await expect(controller.queue("https://example.com/song.mp3")).rejects.toMatchObject({ + await expect( + controller.queue("https://example.com/song.mp3"), + ).rejects.toMatchObject({ code: "VOICE_NOT_CONNECTED", statusCode: 409, } satisfies Partial); @@ -40,7 +46,9 @@ describe("MediaController", () => { musicPlayer: { play: vi.fn() }, }); - await expect(controller.queue("https://example.com/song.mp3")).rejects.toMatchObject({ + await expect( + controller.queue("https://example.com/song.mp3"), + ).rejects.toMatchObject({ code: "BROWSER_STREAM_ACTIVE", statusCode: 409, } satisfies Partial); @@ -94,7 +102,10 @@ describe("MediaController", () => { const musicPlayer: MusicPlayer = { play: vi .fn() - .mockReturnValueOnce({ done: new Promise(() => {}), stop: currentStop }) + .mockReturnValueOnce({ + done: new Promise(() => {}), + stop: currentStop, + }) .mockReturnValueOnce({ done: nextPlayback.promise, stop: vi.fn() }), }; const controller = new MediaController({ @@ -170,7 +181,9 @@ describe("MediaController", () => { isVoiceConnected: () => true, isBrowserStreaming: () => false, resolveMediaSource: async (input) => source(input), - musicPlayer: { play: vi.fn(() => ({ done: new Promise(() => {}), stop })) }, + musicPlayer: { + play: vi.fn(() => ({ done: new Promise(() => {}), stop })), + }, }); await controller.queue("https://example.com/song.mp3"); @@ -186,7 +199,9 @@ describe("MediaController", () => { isVoiceConnected: () => true, isBrowserStreaming: () => false, resolveMediaSource: async (input) => source(input), - musicPlayer: { play: vi.fn(() => ({ done: new Promise(() => {}), stop: vi.fn() })) }, + musicPlayer: { + play: vi.fn(() => ({ done: new Promise(() => {}), stop: vi.fn() })), + }, onStateChange, }); diff --git a/tests/media/mediaQueue.test.ts b/tests/media/mediaQueue.test.ts index bb21531..7f450c2 100644 --- a/tests/media/mediaQueue.test.ts +++ b/tests/media/mediaQueue.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest"; import { MediaQueue } from "../../src/media/mediaQueue"; import type { ResolvedMediaSource } from "../../src/media/mediaTypes"; -function source(overrides: Partial = {}): ResolvedMediaSource { +function source( + overrides: Partial = {}, +): ResolvedMediaSource { return { source: "https://example.com/audio.ogg", title: "audio.ogg", @@ -13,7 +15,10 @@ function source(overrides: Partial = {}): ResolvedMediaSour describe("MediaQueue", () => { it("adds items with stable queue metadata", () => { - const queue = new MediaQueue(() => "item-1", () => 1700000000000); + const queue = new MediaQueue( + () => "item-1", + () => 1700000000000, + ); const item = queue.add(source(), "tester"); @@ -31,7 +36,10 @@ describe("MediaQueue", () => { }); it("marks the next queued item as playing", () => { - const queue = new MediaQueue(() => "item-1", () => 1700000000000); + const queue = new MediaQueue( + () => "item-1", + () => 1700000000000, + ); const item = queue.add(source(), "tester"); expect(queue.startNext()).toEqual({ ...item, status: "playing" }); @@ -43,7 +51,10 @@ describe("MediaQueue", () => { it("removes current item and starts following item", () => { let id = 0; - const queue = new MediaQueue(() => `item-${++id}`, () => 1700000000000); + const queue = new MediaQueue( + () => `item-${++id}`, + () => 1700000000000, + ); queue.add(source({ title: "first" }), "tester"); queue.add(source({ title: "second" }), "tester"); queue.startNext(); @@ -56,7 +67,10 @@ describe("MediaQueue", () => { }); it("returns the failed current item", () => { - const queue = new MediaQueue(() => "item-1", () => 1700000000000); + const queue = new MediaQueue( + () => "item-1", + () => 1700000000000, + ); const item = queue.add(source(), "tester"); queue.startNext(); @@ -65,7 +79,10 @@ describe("MediaQueue", () => { }); it("clears current and queued items", () => { - const queue = new MediaQueue(() => "item-1", () => 1700000000000); + const queue = new MediaQueue( + () => "item-1", + () => 1700000000000, + ); queue.add(source(), "tester"); queue.startNext(); diff --git a/tests/media/mediaResolver.test.ts b/tests/media/mediaResolver.test.ts index 1f6c8dc..30d341c 100644 --- a/tests/media/mediaResolver.test.ts +++ b/tests/media/mediaResolver.test.ts @@ -7,7 +7,9 @@ import { resolveMediaSource } from "../../src/media/mediaResolver"; describe("resolveMediaSource", () => { it("accepts http URLs", async () => { - await expect(resolveMediaSource("https://example.com/music.mp3")).resolves.toEqual({ + await expect( + resolveMediaSource("https://example.com/music.mp3"), + ).resolves.toEqual({ source: "https://example.com/music.mp3", title: "music.mp3", kind: "url", @@ -43,14 +45,18 @@ describe("resolveMediaSource", () => { }); it("rejects unsupported sources", async () => { - await expect(resolveMediaSource("not a url or file")).rejects.toMatchObject({ - code: "UNSUPPORTED_MEDIA_SOURCE", - statusCode: 400, - } satisfies Partial); + await expect(resolveMediaSource("not a url or file")).rejects.toMatchObject( + { + code: "UNSUPPORTED_MEDIA_SOURCE", + statusCode: 400, + } satisfies Partial, + ); }); it("rejects non-http URL sources", async () => { - await expect(resolveMediaSource("file:///tmp/song.mp3")).rejects.toMatchObject({ + await expect( + resolveMediaSource("file:///tmp/song.mp3"), + ).rejects.toMatchObject({ code: "UNSUPPORTED_MEDIA_SOURCE", statusCode: 400, } satisfies Partial); diff --git a/tests/media/musicPlayer.test.ts b/tests/media/musicPlayer.test.ts index dc46936..3f18580 100644 --- a/tests/media/musicPlayer.test.ts +++ b/tests/media/musicPlayer.test.ts @@ -1,8 +1,8 @@ import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; import { describe, expect, it, vi } from "vitest"; -import { createMusicPlayer } from "../../src/media/musicPlayer"; import type { DiscordAudioPlayer } from "../../src/media/mediaTypes"; +import { createMusicPlayer } from "../../src/media/musicPlayer"; class FakeProcess extends EventEmitter { stdout = new PassThrough(); @@ -34,23 +34,27 @@ describe("createMusicPlayer", () => { proc.emit("close", 0); await playback.done; - expect(spawn).toHaveBeenCalledWith("ffmpeg", [ - "-hide_banner", - "-loglevel", - "warning", - "-i", - "https://example.com/song.mp3", - "-vn", - "-acodec", - "libopus", - "-ar", - "48000", - "-ac", - "2", - "-f", - "ogg", - "pipe:1", - ], { stdio: ["ignore", "pipe", "pipe"] }); + expect(spawn).toHaveBeenCalledWith( + "ffmpeg", + [ + "-hide_banner", + "-loglevel", + "warning", + "-i", + "https://example.com/song.mp3", + "-vn", + "-acodec", + "libopus", + "-ar", + "48000", + "-ac", + "2", + "-f", + "ogg", + "pipe:1", + ], + { stdio: ["ignore", "pipe", "pipe"] }, + ); expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout); }); @@ -64,7 +68,11 @@ describe("createMusicPlayer", () => { const player = createMusicPlayer({ spawn, discordPlayer }); expect(() => - player.play({ source: "/tmp/song.ogg", title: "song.ogg", kind: "local" }), + player.play({ + source: "/tmp/song.ogg", + title: "song.ogg", + kind: "local", + }), ).toThrow("Discord audio player is not connected"); expect(spawn).not.toHaveBeenCalled(); }); @@ -76,9 +84,16 @@ describe("createMusicPlayer", () => { playStream: vi.fn(), stop: vi.fn(), }; - const player = createMusicPlayer({ spawn: vi.fn(() => proc), discordPlayer }); + const player = createMusicPlayer({ + spawn: vi.fn(() => proc), + discordPlayer, + }); - const playback = player.play({ source: "/tmp/song.ogg", title: "song.ogg", kind: "local" }); + const playback = player.play({ + source: "/tmp/song.ogg", + title: "song.ogg", + kind: "local", + }); playback.stop(); playback.stop(); @@ -86,4 +101,4 @@ describe("createMusicPlayer", () => { expect(proc.kill).toHaveBeenCalledWith("SIGTERM"); expect(discordPlayer.stop).toHaveBeenCalledTimes(1); }); -}); \ No newline at end of file +}); diff --git a/tests/routes/mediaRoutes.test.ts b/tests/routes/mediaRoutes.test.ts index 3a1095f..5634044 100644 --- a/tests/routes/mediaRoutes.test.ts +++ b/tests/routes/mediaRoutes.test.ts @@ -2,7 +2,11 @@ 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) { +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; } @@ -15,12 +19,20 @@ describe("createMediaRoutes", () => { skip: vi.fn(), stop: vi.fn(), }; - const handler = getHandler(createMediaRoutes(controller), "/media/status", "get"); + 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: [] }); + expect(json).toHaveBeenCalledWith({ + playing: false, + current: null, + queue: [], + }); }); it("queues a source", async () => { @@ -31,7 +43,11 @@ describe("createMediaRoutes", () => { skip: vi.fn(), stop: vi.fn(), }; - const handler = getHandler(createMediaRoutes(controller), "/media/queue", "post"); + const handler = getHandler( + createMediaRoutes(controller), + "/media/queue", + "post", + ); const json = vi.fn(); await handler?.( @@ -40,7 +56,9 @@ describe("createMediaRoutes", () => { vi.fn(), ); - expect(controller.queue).toHaveBeenCalledWith("https://example.com/song.mp3"); + expect(controller.queue).toHaveBeenCalledWith( + "https://example.com/song.mp3", + ); expect(json).toHaveBeenCalledWith(state); }); @@ -51,7 +69,11 @@ describe("createMediaRoutes", () => { skip: vi.fn(), stop: vi.fn(), }; - const handler = getHandler(createMediaRoutes(controller), "/media/queue", "post"); + const handler = getHandler( + createMediaRoutes(controller), + "/media/queue", + "post", + ); const next = vi.fn(); await handler?.(