feat(core): implement data retention, metrics, and enhanced media handling
This commit introduces several significant improvements across the backend and gateway services:
- **Data Retention**: Added an automated cleanup scheduler in `discord-gateway` to prune expired messages, attachments, and voice recordings based on configurable retention policies.
- **Observability**: Integrated `prom-client` in the `backend` service to expose Prometheus metrics via `/api/metrics` and added default Node.js runtime metrics.
- **Media Handling**: Enhanced `MediaHandler` in `discord-gateway` to support media URL resolution and improved playback status tracking.
- **API & Config**: Expanded the configuration endpoint to expose more system settings and reorganized `.env.example` for better readability.
- **Refactoring & Cleanup**:
- Removed unused `better-sqlite3` dependency.
- Refactored voice channel routing.
- Improved error handling and testing coverage with comprehensive unit tests for shared utilities and error classes.
- **Documentation**: Added `MEMORY.md` for project context.
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
import { type CommandMessage, type CommandReply } from "@bete/shared";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { StreamType } from "@discordjs/voice";
|
||||
import { resolveMediaUrl } from "../voice-recording/mediaSource.js";
|
||||
import { discordPlayer } from "../voice-recording/player.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface CurrentTrack {
|
||||
title: string;
|
||||
url: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface MediaStatusPayload {
|
||||
playing: boolean;
|
||||
musicVolume: number;
|
||||
current: unknown;
|
||||
current: CurrentTrack | null;
|
||||
queue: unknown[];
|
||||
}
|
||||
|
||||
@@ -19,29 +27,84 @@ export interface MediaStatusPayload {
|
||||
|
||||
export class MediaHandler {
|
||||
private logger = createChildLogger("media-handler");
|
||||
private currentTrack: CurrentTrack | null = null;
|
||||
|
||||
getCurrentMediaStatus(): MediaStatusPayload {
|
||||
return {
|
||||
playing: discordPlayer.getStatus() === "playing",
|
||||
musicVolume: discordPlayer.getMusicVolume(),
|
||||
current: null,
|
||||
current: this.currentTrack,
|
||||
queue: [],
|
||||
};
|
||||
}
|
||||
|
||||
async handleMediaQueue(cmd: CommandMessage): Promise<CommandReply<unknown>> {
|
||||
this.logger.info(
|
||||
"media:queue received — media queueing is handled externally",
|
||||
);
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: true,
|
||||
data: this.getCurrentMediaStatus(),
|
||||
};
|
||||
const url = String(cmd.payload.url ?? "").trim();
|
||||
|
||||
if (!url) {
|
||||
this.logger.warn("media:queue received without a URL");
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: false,
|
||||
data: null,
|
||||
error: "url is required",
|
||||
};
|
||||
}
|
||||
|
||||
if (!discordPlayer.isConnected()) {
|
||||
this.logger.warn(
|
||||
"media:queue attempted without an active voice connection",
|
||||
);
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: false,
|
||||
data: null,
|
||||
error: "Not connected to a voice channel. Connect to voice first.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.info({ url }, "Resolving media URL");
|
||||
|
||||
const resolution = await resolveMediaUrl(url);
|
||||
|
||||
this.currentTrack = {
|
||||
title: resolution.title ?? url,
|
||||
url,
|
||||
duration: resolution.duration,
|
||||
};
|
||||
|
||||
discordPlayer.playStream(resolution.stream, "music", {
|
||||
inputType: StreamType.Arbitrary,
|
||||
inlineVolume: true,
|
||||
volume: discordPlayer.getMusicVolume(),
|
||||
});
|
||||
|
||||
this.logger.info(
|
||||
{ url, title: resolution.title },
|
||||
"Media queued and playback started",
|
||||
);
|
||||
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: true,
|
||||
data: this.getCurrentMediaStatus(),
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this.logger.error({ error: message, url }, "Failed to queue media");
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: false,
|
||||
data: null,
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async handleMediaSkip(cmd: CommandMessage): Promise<CommandReply<unknown>> {
|
||||
discordPlayer.stop("music");
|
||||
this.currentTrack = null;
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: true,
|
||||
@@ -51,6 +114,7 @@ export class MediaHandler {
|
||||
|
||||
async handleMediaStop(cmd: CommandMessage): Promise<CommandReply<unknown>> {
|
||||
discordPlayer.stop("music");
|
||||
this.currentTrack = null;
|
||||
return {
|
||||
id: cmd.id,
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user