style: format media music implementation
This commit is contained in:
@@ -2,13 +2,13 @@ import { AppError } from "../errors";
|
|||||||
import { discordPlayer } from "../player";
|
import { discordPlayer } from "../player";
|
||||||
import { MediaQueue } from "./mediaQueue";
|
import { MediaQueue } from "./mediaQueue";
|
||||||
import { resolveMediaSource } from "./mediaResolver";
|
import { resolveMediaSource } from "./mediaResolver";
|
||||||
import { createMusicPlayer } from "./musicPlayer";
|
|
||||||
import type {
|
import type {
|
||||||
MediaState,
|
MediaState,
|
||||||
MusicPlayback,
|
MusicPlayback,
|
||||||
MusicPlayer,
|
MusicPlayer,
|
||||||
ResolvedMediaSource,
|
ResolvedMediaSource,
|
||||||
} from "./mediaTypes";
|
} from "./mediaTypes";
|
||||||
|
import { createMusicPlayer } from "./musicPlayer";
|
||||||
|
|
||||||
export interface MediaControllerDependencies {
|
export interface MediaControllerDependencies {
|
||||||
isVoiceConnected?: () => boolean;
|
isVoiceConnected?: () => boolean;
|
||||||
@@ -39,9 +39,9 @@ export class MediaController {
|
|||||||
|
|
||||||
async queue(source: string): Promise<MediaState> {
|
async queue(source: string): Promise<MediaState> {
|
||||||
this.assertCanStart();
|
this.assertCanStart();
|
||||||
const resolved = await (this.dependencies.resolveMediaSource ?? resolveMediaSource)(
|
const resolved = await (
|
||||||
source,
|
this.dependencies.resolveMediaSource ?? resolveMediaSource
|
||||||
);
|
)(source);
|
||||||
this.queueStore.add(resolved);
|
this.queueStore.add(resolved);
|
||||||
this.startNextIfIdle();
|
this.startNextIfIdle();
|
||||||
return this.emitState();
|
return this.emitState();
|
||||||
@@ -49,7 +49,11 @@ export class MediaController {
|
|||||||
|
|
||||||
async skip(): Promise<MediaState> {
|
async skip(): Promise<MediaState> {
|
||||||
if (this.skipInProgress) {
|
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;
|
this.skipInProgress = true;
|
||||||
@@ -74,8 +78,8 @@ export class MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private assertCanStart(): void {
|
private assertCanStart(): void {
|
||||||
const isVoiceConnected = this.dependencies.isVoiceConnected ??
|
const isVoiceConnected =
|
||||||
(() => discordPlayer.isConnected());
|
this.dependencies.isVoiceConnected ?? (() => discordPlayer.isConnected());
|
||||||
if (!isVoiceConnected()) {
|
if (!isVoiceConnected()) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
"Connect to a voice channel before playing media",
|
"Connect to a voice channel before playing media",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn as nodeSpawn } from "node:child_process";
|
|
||||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
|
import { spawn as nodeSpawn } from "node:child_process";
|
||||||
import { discordPlayer } from "../player";
|
import { discordPlayer } from "../player";
|
||||||
import type {
|
import type {
|
||||||
DiscordAudioPlayer,
|
DiscordAudioPlayer,
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export function createMediaRoutes(controller: MediaRouteController): Router {
|
|||||||
try {
|
try {
|
||||||
const { source } = req.body as { source?: string };
|
const { source } = req.body as { source?: string };
|
||||||
if (!source) {
|
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));
|
res.json(await controller.queue(source));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as prism from "prism-media";
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { AppError } from "./errors";
|
import { AppError } from "./errors";
|
||||||
import { createChildLogger, logger } from "./logger";
|
import { createChildLogger, logger } from "./logger";
|
||||||
|
import { MediaController } from "./media/mediaController";
|
||||||
import { getMetrics, uptimeGauge } from "./metrics";
|
import { getMetrics, uptimeGauge } from "./metrics";
|
||||||
import { createBroadcaster } from "./moderation/broadcaster";
|
import { createBroadcaster } from "./moderation/broadcaster";
|
||||||
import type { ModerationBroadcaster } from "./moderation/types";
|
import type { ModerationBroadcaster } from "./moderation/types";
|
||||||
@@ -19,7 +20,6 @@ import { createMessageRoutes } from "./routes/messageRoutes";
|
|||||||
import { createSyncRoutes } from "./routes/syncRoutes";
|
import { createSyncRoutes } from "./routes/syncRoutes";
|
||||||
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
||||||
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
||||||
import { MediaController } from "./media/mediaController";
|
|
||||||
import type { VoiceController } from "./voiceController";
|
import type { VoiceController } from "./voiceController";
|
||||||
|
|
||||||
const wsLogger = createChildLogger("webserver");
|
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: "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[]) => {
|
ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => {
|
||||||
if (!Buffer.isBuffer(data)) return;
|
if (!Buffer.isBuffer(data)) return;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { AppError } from "../../src/errors";
|
import { AppError } from "../../src/errors";
|
||||||
import { MediaController } from "../../src/media/mediaController";
|
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() {
|
function deferred() {
|
||||||
let resolve!: () => void;
|
let resolve!: () => void;
|
||||||
@@ -26,7 +30,9 @@ describe("MediaController", () => {
|
|||||||
musicPlayer: { play: vi.fn() },
|
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",
|
code: "VOICE_NOT_CONNECTED",
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
} satisfies Partial<AppError>);
|
} satisfies Partial<AppError>);
|
||||||
@@ -40,7 +46,9 @@ describe("MediaController", () => {
|
|||||||
musicPlayer: { play: vi.fn() },
|
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",
|
code: "BROWSER_STREAM_ACTIVE",
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
} satisfies Partial<AppError>);
|
} satisfies Partial<AppError>);
|
||||||
@@ -94,7 +102,10 @@ describe("MediaController", () => {
|
|||||||
const musicPlayer: MusicPlayer = {
|
const musicPlayer: MusicPlayer = {
|
||||||
play: vi
|
play: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValueOnce({ done: new Promise<void>(() => {}), stop: currentStop })
|
.mockReturnValueOnce({
|
||||||
|
done: new Promise<void>(() => {}),
|
||||||
|
stop: currentStop,
|
||||||
|
})
|
||||||
.mockReturnValueOnce({ done: nextPlayback.promise, stop: vi.fn() }),
|
.mockReturnValueOnce({ done: nextPlayback.promise, stop: vi.fn() }),
|
||||||
};
|
};
|
||||||
const controller = new MediaController({
|
const controller = new MediaController({
|
||||||
@@ -170,7 +181,9 @@ describe("MediaController", () => {
|
|||||||
isVoiceConnected: () => true,
|
isVoiceConnected: () => true,
|
||||||
isBrowserStreaming: () => false,
|
isBrowserStreaming: () => false,
|
||||||
resolveMediaSource: async (input) => source(input),
|
resolveMediaSource: async (input) => source(input),
|
||||||
musicPlayer: { play: vi.fn(() => ({ done: new Promise<void>(() => {}), stop })) },
|
musicPlayer: {
|
||||||
|
play: vi.fn(() => ({ done: new Promise<void>(() => {}), stop })),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await controller.queue("https://example.com/song.mp3");
|
await controller.queue("https://example.com/song.mp3");
|
||||||
|
|
||||||
@@ -186,7 +199,9 @@ describe("MediaController", () => {
|
|||||||
isVoiceConnected: () => true,
|
isVoiceConnected: () => true,
|
||||||
isBrowserStreaming: () => false,
|
isBrowserStreaming: () => false,
|
||||||
resolveMediaSource: async (input) => source(input),
|
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,
|
onStateChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { MediaQueue } from "../../src/media/mediaQueue";
|
import { MediaQueue } from "../../src/media/mediaQueue";
|
||||||
import type { ResolvedMediaSource } from "../../src/media/mediaTypes";
|
import type { ResolvedMediaSource } from "../../src/media/mediaTypes";
|
||||||
|
|
||||||
function source(overrides: Partial<ResolvedMediaSource> = {}): ResolvedMediaSource {
|
function source(
|
||||||
|
overrides: Partial<ResolvedMediaSource> = {},
|
||||||
|
): ResolvedMediaSource {
|
||||||
return {
|
return {
|
||||||
source: "https://example.com/audio.ogg",
|
source: "https://example.com/audio.ogg",
|
||||||
title: "audio.ogg",
|
title: "audio.ogg",
|
||||||
@@ -13,7 +15,10 @@ function source(overrides: Partial<ResolvedMediaSource> = {}): ResolvedMediaSour
|
|||||||
|
|
||||||
describe("MediaQueue", () => {
|
describe("MediaQueue", () => {
|
||||||
it("adds items with stable queue metadata", () => {
|
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");
|
const item = queue.add(source(), "tester");
|
||||||
|
|
||||||
@@ -31,7 +36,10 @@ describe("MediaQueue", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("marks the next queued item as playing", () => {
|
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");
|
const item = queue.add(source(), "tester");
|
||||||
|
|
||||||
expect(queue.startNext()).toEqual({ ...item, status: "playing" });
|
expect(queue.startNext()).toEqual({ ...item, status: "playing" });
|
||||||
@@ -43,7 +51,10 @@ describe("MediaQueue", () => {
|
|||||||
|
|
||||||
it("removes current item and starts following item", () => {
|
it("removes current item and starts following item", () => {
|
||||||
let id = 0;
|
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: "first" }), "tester");
|
||||||
queue.add(source({ title: "second" }), "tester");
|
queue.add(source({ title: "second" }), "tester");
|
||||||
queue.startNext();
|
queue.startNext();
|
||||||
@@ -56,7 +67,10 @@ describe("MediaQueue", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns the failed current item", () => {
|
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");
|
const item = queue.add(source(), "tester");
|
||||||
queue.startNext();
|
queue.startNext();
|
||||||
|
|
||||||
@@ -65,7 +79,10 @@ describe("MediaQueue", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clears current and queued items", () => {
|
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.add(source(), "tester");
|
||||||
queue.startNext();
|
queue.startNext();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { resolveMediaSource } from "../../src/media/mediaResolver";
|
|||||||
|
|
||||||
describe("resolveMediaSource", () => {
|
describe("resolveMediaSource", () => {
|
||||||
it("accepts http URLs", async () => {
|
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",
|
source: "https://example.com/music.mp3",
|
||||||
title: "music.mp3",
|
title: "music.mp3",
|
||||||
kind: "url",
|
kind: "url",
|
||||||
@@ -43,14 +45,18 @@ describe("resolveMediaSource", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects unsupported sources", async () => {
|
it("rejects unsupported sources", async () => {
|
||||||
await expect(resolveMediaSource("not a url or file")).rejects.toMatchObject({
|
await expect(resolveMediaSource("not a url or file")).rejects.toMatchObject(
|
||||||
|
{
|
||||||
code: "UNSUPPORTED_MEDIA_SOURCE",
|
code: "UNSUPPORTED_MEDIA_SOURCE",
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
} satisfies Partial<AppError>);
|
} satisfies Partial<AppError>,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects non-http URL sources", async () => {
|
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",
|
code: "UNSUPPORTED_MEDIA_SOURCE",
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
} satisfies Partial<AppError>);
|
} satisfies Partial<AppError>);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { PassThrough } from "node:stream";
|
import { PassThrough } from "node:stream";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { createMusicPlayer } from "../../src/media/musicPlayer";
|
|
||||||
import type { DiscordAudioPlayer } from "../../src/media/mediaTypes";
|
import type { DiscordAudioPlayer } from "../../src/media/mediaTypes";
|
||||||
|
import { createMusicPlayer } from "../../src/media/musicPlayer";
|
||||||
|
|
||||||
class FakeProcess extends EventEmitter {
|
class FakeProcess extends EventEmitter {
|
||||||
stdout = new PassThrough();
|
stdout = new PassThrough();
|
||||||
@@ -34,7 +34,9 @@ describe("createMusicPlayer", () => {
|
|||||||
proc.emit("close", 0);
|
proc.emit("close", 0);
|
||||||
await playback.done;
|
await playback.done;
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith("ffmpeg", [
|
expect(spawn).toHaveBeenCalledWith(
|
||||||
|
"ffmpeg",
|
||||||
|
[
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"warning",
|
"warning",
|
||||||
@@ -50,7 +52,9 @@ describe("createMusicPlayer", () => {
|
|||||||
"-f",
|
"-f",
|
||||||
"ogg",
|
"ogg",
|
||||||
"pipe:1",
|
"pipe:1",
|
||||||
], { stdio: ["ignore", "pipe", "pipe"] });
|
],
|
||||||
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout);
|
expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,7 +68,11 @@ describe("createMusicPlayer", () => {
|
|||||||
const player = createMusicPlayer({ spawn, discordPlayer });
|
const player = createMusicPlayer({ spawn, discordPlayer });
|
||||||
|
|
||||||
expect(() =>
|
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");
|
).toThrow("Discord audio player is not connected");
|
||||||
expect(spawn).not.toHaveBeenCalled();
|
expect(spawn).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -76,9 +84,16 @@ describe("createMusicPlayer", () => {
|
|||||||
playStream: vi.fn(),
|
playStream: vi.fn(),
|
||||||
stop: 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();
|
||||||
playback.stop();
|
playback.stop();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import type { Request, Response } from "express";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { createMediaRoutes } from "../../src/routes/mediaRoutes";
|
import { createMediaRoutes } from "../../src/routes/mediaRoutes";
|
||||||
|
|
||||||
function getHandler(router: ReturnType<typeof createMediaRoutes>, path: string, method: string) {
|
function getHandler(
|
||||||
|
router: ReturnType<typeof createMediaRoutes>,
|
||||||
|
path: string,
|
||||||
|
method: string,
|
||||||
|
) {
|
||||||
const layer = router.stack.find((item) => item.route?.path === path);
|
const layer = router.stack.find((item) => item.route?.path === path);
|
||||||
return layer?.route?.stack.find((item) => item.method === method)?.handle;
|
return layer?.route?.stack.find((item) => item.method === method)?.handle;
|
||||||
}
|
}
|
||||||
@@ -15,12 +19,20 @@ describe("createMediaRoutes", () => {
|
|||||||
skip: vi.fn(),
|
skip: vi.fn(),
|
||||||
stop: 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();
|
const json = vi.fn();
|
||||||
|
|
||||||
await handler?.({} as Request, { json } as unknown as Response, 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 () => {
|
it("queues a source", async () => {
|
||||||
@@ -31,7 +43,11 @@ describe("createMediaRoutes", () => {
|
|||||||
skip: vi.fn(),
|
skip: vi.fn(),
|
||||||
stop: 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();
|
const json = vi.fn();
|
||||||
|
|
||||||
await handler?.(
|
await handler?.(
|
||||||
@@ -40,7 +56,9 @@ describe("createMediaRoutes", () => {
|
|||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(controller.queue).toHaveBeenCalledWith("https://example.com/song.mp3");
|
expect(controller.queue).toHaveBeenCalledWith(
|
||||||
|
"https://example.com/song.mp3",
|
||||||
|
);
|
||||||
expect(json).toHaveBeenCalledWith(state);
|
expect(json).toHaveBeenCalledWith(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +69,11 @@ describe("createMediaRoutes", () => {
|
|||||||
skip: vi.fn(),
|
skip: vi.fn(),
|
||||||
stop: 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();
|
const next = vi.fn();
|
||||||
|
|
||||||
await handler?.(
|
await handler?.(
|
||||||
|
|||||||
Reference in New Issue
Block a user