Four things people hit while using the panel: - The bot could be sent into a channel the requester was not in, and playback could be started from nowhere. Playback now follows the listener (REQUIRE_LISTENER, on by default), and the voice row shows where you and the bot are instead of offering a free channel picker. - Voice presence only refreshed on reload, because the SDK updates channel participants without emitting an event. The socket now watches that view and pushes changes. - The bot left the channel whenever the queue ran dry. It now leaves only after the last person does, EMPTY_TIMEOUT_SECONDS later (120 by default), and stays put while anyone is still listening. - A search that yielded nothing said nothing: yt-dlp can exit 0 with an empty result, so that case now reports the reason (or "nothing found"), and searches are logged with their result count. The queue moved under the player so search owns the left column, and elapsed time no longer renders as "LIVE" — formatDuration treated 0 as a live stream, which also affected the chat's progress bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { startApiServer } from "./api/server.js";
|
|
import { startBot } from "./bot/index.js";
|
|
import { config } from "./config.js";
|
|
import { MusicManager } from "./core/manager.js";
|
|
import { logger } from "./logger.js";
|
|
import { checkCookies, checkYtDlp } from "./sources/index.js";
|
|
|
|
async function main(): Promise<void> {
|
|
const ytdlpVersion = await checkYtDlp();
|
|
if (ytdlpVersion) {
|
|
logger.info({ version: ytdlpVersion }, "yt-dlp detected");
|
|
} else {
|
|
logger.warn(
|
|
{ path: config.YTDLP_PATH },
|
|
"yt-dlp not found — YouTube/SoundCloud playback will fail; only direct links and local files will work",
|
|
);
|
|
}
|
|
|
|
if (process.env["IDLE_TIMEOUT_SECONDS"]) {
|
|
logger.warn(
|
|
"IDLE_TIMEOUT_SECONDS is gone: the bot now leaves only when the voice channel empties — use EMPTY_TIMEOUT_SECONDS",
|
|
);
|
|
}
|
|
|
|
const cookies = await checkCookies();
|
|
if (cookies === "ok") {
|
|
logger.info({ path: config.YTDLP_COOKIES }, "using YouTube cookies");
|
|
} else if (cookies === "read-only") {
|
|
logger.warn(
|
|
{ path: config.YTDLP_COOKIES },
|
|
"cookie file is not writable — yt-dlp cannot persist rotated cookies and the session will expire early",
|
|
);
|
|
} else if (cookies === "missing") {
|
|
logger.warn({ path: config.YTDLP_COOKIES }, "cookie file from YTDLP_COOKIES does not exist");
|
|
}
|
|
|
|
const manager = new MusicManager();
|
|
const bot = await startBot(manager);
|
|
const app = await startApiServer({ manager, context: bot.context });
|
|
|
|
const shutdown = async (signal: string): Promise<void> => {
|
|
logger.info({ signal }, "shutting down");
|
|
try {
|
|
await app.close();
|
|
await bot.stop();
|
|
} catch (err) {
|
|
logger.error({ err }, "shutdown failed");
|
|
} finally {
|
|
process.exit(0);
|
|
}
|
|
};
|
|
|
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
process.on("unhandledRejection", (err) => logger.error({ err }, "unhandled rejection"));
|
|
// revoice.js throws from async callbacks we cannot wrap (its ffmpeg teardown
|
|
// in particular). Dying there would drop the bot out of the voice channel
|
|
// mid-track, so we log loudly and keep serving instead.
|
|
process.on("uncaughtException", (err) => logger.error({ err }, "uncaught exception, continuing"));
|
|
}
|
|
|
|
main().catch((err) => {
|
|
logger.fatal({ err }, "failed to start");
|
|
process.exit(1);
|
|
});
|