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-09 19:46:08 +07:00
|
|
|
import {
|
|
|
|
|
createChildLogger,
|
|
|
|
|
tryCommandThenFallback,
|
|
|
|
|
} from "../../shared/commandHelper.js";
|
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: [],
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-09 19:46:08 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Normalisation — handle both boolean (new) and string (legacy) playing values
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function normalizeMediaState(raw: Record<string, unknown>): MediaState {
|
|
|
|
|
const rawPlaying = raw.playing;
|
|
|
|
|
const playing =
|
|
|
|
|
rawPlaying === true ||
|
|
|
|
|
rawPlaying === "playing" ||
|
|
|
|
|
rawPlaying === "buffering";
|
|
|
|
|
return {
|
|
|
|
|
playing,
|
|
|
|
|
musicVolume: Number(raw.musicVolume ?? 1.0),
|
|
|
|
|
current: (raw.current as MediaItem | null) ?? null,
|
|
|
|
|
queue: (raw.queue as MediaItem[]) ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MediaReplyData = Record<string, unknown> | MediaState;
|
|
|
|
|
|
2026-07-26 14:27:36 +07:00
|
|
|
function _fromReply(data: MediaReplyData): MediaState {
|
2026-06-09 19:46:08 +07:00
|
|
|
return normalizeMediaState(data as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readStatusFallback(): Promise<MediaState> {
|
|
|
|
|
const cached = await readRedisStatus(MEDIA_STATUS_KEY);
|
|
|
|
|
return cached ? normalizeMediaState(cached) : DEFAULT_STATE;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 00:32:38 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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");
|
2026-06-09 19:46:08 +07:00
|
|
|
return readStatusFallback();
|
2026-06-02 00:32:38 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-09 19:46:08 +07:00
|
|
|
return tryCommandThenFallback(
|
|
|
|
|
() =>
|
|
|
|
|
publishCommand<MediaState>(
|
|
|
|
|
COMMAND_MEDIA_QUEUE,
|
|
|
|
|
{ source, mode },
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
),
|
|
|
|
|
() => readStatusFallback(),
|
|
|
|
|
"queue",
|
2026-06-02 00:32:38 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-09 19:46:08 +07:00
|
|
|
return tryCommandThenFallback(
|
|
|
|
|
() =>
|
|
|
|
|
publishCommand<MediaState>(
|
|
|
|
|
COMMAND_MEDIA_SKIP,
|
|
|
|
|
{},
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
),
|
|
|
|
|
() => readStatusFallback(),
|
|
|
|
|
"skip",
|
2026-06-02 00:32:38 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-09 19:46:08 +07:00
|
|
|
return tryCommandThenFallback(
|
|
|
|
|
() =>
|
|
|
|
|
publishCommand<MediaState>(
|
|
|
|
|
COMMAND_MEDIA_STOP,
|
|
|
|
|
{},
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
),
|
|
|
|
|
() => readStatusFallback(),
|
|
|
|
|
"stop",
|
2026-06-02 00:32:38 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-09 19:46:08 +07:00
|
|
|
return tryCommandThenFallback(
|
|
|
|
|
() =>
|
|
|
|
|
publishCommand<MediaState>(
|
|
|
|
|
COMMAND_MEDIA_VOLUME,
|
|
|
|
|
{ volume },
|
|
|
|
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
|
|
|
),
|
|
|
|
|
() => readStatusFallback(),
|
|
|
|
|
"setVolume",
|
2026-06-02 00:32:38 +07:00
|
|
|
);
|
|
|
|
|
}
|