2026-06-09 17:34:18 +07:00
|
|
|
import {
|
|
|
|
|
COMMAND_MEDIA_QUEUE,
|
|
|
|
|
COMMAND_MEDIA_SKIP,
|
|
|
|
|
COMMAND_MEDIA_STOP,
|
|
|
|
|
COMMAND_MEDIA_VOLUME,
|
|
|
|
|
MEDIA_STATUS_KEY,
|
|
|
|
|
} from "@bete/shared";
|
2026-06-02 21:06:42 +07:00
|
|
|
import { createChildLogger } from "@bete/shared/logger";
|
2026-06-02 00:32:38 +07:00
|
|
|
import { publishCommand, readRedisStatus } from "../../shared/redis/index.js";
|
2026-06-01 21:44:29 +07:00
|
|
|
|
|
|
|
|
const logger = createChildLogger("media.service");
|
|
|
|
|
|
2026-06-02 00:32:38 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Types — match frontend exactly
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export interface MediaItem {
|
|
|
|
|
id?: string;
|
|
|
|
|
source: string;
|
|
|
|
|
title: string;
|
|
|
|
|
mode?: "music" | "screen";
|
|
|
|
|
durationMs?: number | null;
|
|
|
|
|
thumbnailUrl?: string | null;
|
2026-06-01 21:44:29 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 00:32:38 +07:00
|
|
|
export interface MediaState {
|
|
|
|
|
playing: boolean;
|
|
|
|
|
musicVolume: number;
|
|
|
|
|
current: MediaItem | null;
|
|
|
|
|
queue: MediaItem[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Defaults
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const DEFAULT_COMMAND_TIMEOUT_MS = 5000;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_STATE: MediaState = {
|
|
|
|
|
playing: false,
|
|
|
|
|
musicVolume: 1.0,
|
|
|
|
|
current: null,
|
|
|
|
|
queue: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Service methods
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
2026-06-09 17:34:18 +07:00
|
|
|
* Read media status from Redis key `media:status` set by discord-gateway.
|
2026-06-02 00:32:38 +07:00
|
|
|
*/
|
|
|
|
|
export async function getStatus(): Promise<MediaState> {
|
2026-06-09 17:34:18 +07:00
|
|
|
logger.debug("getStatus called");
|
|
|
|
|
const cached = await readRedisStatus(MEDIA_STATUS_KEY);
|
2026-06-02 00:32:38 +07:00
|
|
|
|
|
|
|
|
if (cached) {
|
2026-06-09 16:56:44 +07:00
|
|
|
const rawPlaying = cached.playing;
|
|
|
|
|
// Handle both boolean (new) and string (legacy from String(discordPlayer.getStatus()))
|
|
|
|
|
const playing =
|
|
|
|
|
rawPlaying === true ||
|
|
|
|
|
rawPlaying === "playing" ||
|
|
|
|
|
rawPlaying === "buffering";
|
2026-06-02 00:32:38 +07:00
|
|
|
return {
|
2026-06-09 16:56:44 +07:00
|
|
|
playing,
|
2026-06-02 00:32:38 +07:00
|
|
|
musicVolume: Number(cached.musicVolume ?? 1.0),
|
|
|
|
|
current: (cached.current as MediaItem | null) ?? null,
|
|
|
|
|
queue: (cached.queue as MediaItem[]) ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DEFAULT_STATE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Queue a media source via Redis command to discord-gateway.
|
|
|
|
|
*/
|
|
|
|
|
export async function queue(
|
|
|
|
|
source: string,
|
|
|
|
|
mode: "music" | "screen" = "music",
|
|
|
|
|
): Promise<MediaState> {
|
2026-06-09 17:34:18 +07:00
|
|
|
logger.info({ source, mode }, "queue called");
|
2026-06-02 00:32:38 +07:00
|
|
|
const reply = await publishCommand<MediaState>(
|
2026-06-09 17:34:18 +07:00
|
|
|
COMMAND_MEDIA_QUEUE,
|
2026-06-02 00:32:38 +07:00
|
|
|
{ source, mode },
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (reply?.success && reply.data) {
|
|
|
|
|
return {
|
|
|
|
|
playing: reply.data.playing,
|
|
|
|
|
musicVolume: reply.data.musicVolume,
|
|
|
|
|
current: reply.data.current ?? null,
|
|
|
|
|
queue: reply.data.queue ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
{ source, mode },
|
|
|
|
|
"discord-gateway unreachable, returning current media status",
|
|
|
|
|
);
|
|
|
|
|
return getStatus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Skip current track via Redis command to discord-gateway.
|
|
|
|
|
*/
|
|
|
|
|
export async function skip(): Promise<MediaState> {
|
2026-06-09 17:34:18 +07:00
|
|
|
logger.info("skip called");
|
2026-06-02 00:32:38 +07:00
|
|
|
const reply = await publishCommand<MediaState>(
|
2026-06-09 17:34:18 +07:00
|
|
|
COMMAND_MEDIA_SKIP,
|
2026-06-02 00:32:38 +07:00
|
|
|
{},
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (reply?.success && reply.data) {
|
|
|
|
|
return {
|
|
|
|
|
playing: reply.data.playing,
|
|
|
|
|
musicVolume: reply.data.musicVolume,
|
|
|
|
|
current: reply.data.current ?? null,
|
|
|
|
|
queue: reply.data.queue ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn("discord-gateway unreachable, returning current media status");
|
|
|
|
|
return getStatus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop playback via Redis command to discord-gateway.
|
|
|
|
|
*/
|
|
|
|
|
export async function stop(): Promise<MediaState> {
|
2026-06-09 17:34:18 +07:00
|
|
|
logger.info("stop called");
|
2026-06-02 00:32:38 +07:00
|
|
|
const reply = await publishCommand<MediaState>(
|
2026-06-09 17:34:18 +07:00
|
|
|
COMMAND_MEDIA_STOP,
|
2026-06-02 00:32:38 +07:00
|
|
|
{},
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (reply?.success && reply.data) {
|
|
|
|
|
return {
|
|
|
|
|
playing: reply.data.playing,
|
|
|
|
|
musicVolume: reply.data.musicVolume,
|
|
|
|
|
current: reply.data.current ?? null,
|
|
|
|
|
queue: reply.data.queue ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn("discord-gateway unreachable, returning current media status");
|
|
|
|
|
return getStatus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set volume via Redis command to discord-gateway.
|
|
|
|
|
*/
|
|
|
|
|
export async function setVolume(volume: number): Promise<MediaState> {
|
2026-06-09 17:34:18 +07:00
|
|
|
logger.info({ volume }, "setVolume called");
|
2026-06-02 00:32:38 +07:00
|
|
|
const reply = await publishCommand<MediaState>(
|
2026-06-09 17:34:18 +07:00
|
|
|
COMMAND_MEDIA_VOLUME,
|
2026-06-02 00:32:38 +07:00
|
|
|
{ volume },
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (reply?.success && reply.data) {
|
|
|
|
|
return {
|
|
|
|
|
playing: reply.data.playing,
|
|
|
|
|
musicVolume: reply.data.musicVolume,
|
|
|
|
|
current: reply.data.current ?? null,
|
|
|
|
|
queue: reply.data.queue ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn(
|
|
|
|
|
{ volume },
|
|
|
|
|
"discord-gateway unreachable, returning current media status",
|
|
|
|
|
);
|
|
|
|
|
return getStatus();
|
|
|
|
|
}
|