Answer the search command immediately, and don't wait on a slow source

A search looked like it did nothing: the log shows the two sources
finishing 21 seconds apart, and since the command message is deleted on
sight, the channel stayed empty that whole time with no sign the bot had
heard anything.

The reply now goes out at once as "Ищу …" and is edited into the results,
so there is always something on screen, and each source gets 12 seconds
before the answer goes out without it — searching several at once
otherwise means always waiting for the slowest, which behind a proxy is
tens of seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 05:49:50 +03:00
co-authored by Claude Opus 5
parent e7e80e704b
commit bf2d04d579
3 changed files with 45 additions and 6 deletions
+13 -3
View File
@@ -13,10 +13,11 @@ import {
trackTitle,
} from "./format.js";
/** What we need back from a sent message to hang reactions on it. */
/** What we need back from a sent message to work with it afterwards. */
export interface SentMessage {
id: string;
react(emoji: string): Promise<void>;
edit(content: string): Promise<void>;
}
export interface CommandContext {
@@ -170,18 +171,27 @@ export const commands: Command[] = [
description: "Найти треки и выбрать реакцией",
async run(ctx) {
if (!ctx.rest) throw new UserFacingError("Укажите поисковый запрос");
// Searching several sources can take tens of seconds through a proxy, and
// the command message is already deleted by then — without a placeholder
// the chat sits silent and the bot looks broken.
const placeholder = await ctx.reply(`🔎 Ищу **${ctx.rest}**…`);
const tracks = (await ctx.manager.search(ctx.rest, ctx.actor, CHAT_SEARCH_LIMIT)).slice(
0,
CHAT_SEARCH_LIMIT,
);
if (tracks.length === 0) {
await ctx.reply(`🔎 ${NOTHING_FOUND}`);
const missing = `🔎 ${NOTHING_FOUND}`;
if (placeholder) await placeholder.edit(missing);
else await ctx.reply(missing);
return;
}
const lines = tracks.map((track, index) => trackLine(track, { index: index + 1 }));
const sent = await ctx.reply(`🔎 **${ctx.rest}**\n${lines.join("\n")}`);
const text = `🔎 **${ctx.rest}**\n${lines.join("\n")}`;
const sent = placeholder ?? (await ctx.reply(text));
if (!sent) return;
if (placeholder) await placeholder.edit(text);
rememberSearch(ctx, sent.id, tracks);
for (const emoji of CHOICE_EMOJI.slice(0, tracks.length)) {
+11 -1
View File
@@ -82,7 +82,17 @@ async function handleMessage(manager: MusicManager, message: Message): Promise<v
if (!message.authorId || message.author?.bot) return;
const serverId = message.server?.id;
const reply = async (text: string) => message.channel?.sendMessage(text);
const reply = async (text: string) => {
const sent = await message.channel?.sendMessage(text);
if (!sent) return undefined;
return {
id: sent.id,
react: (emoji: string) => sent.react(emoji),
edit: async (content: string) => {
await sent.edit({ content });
},
};
};
if (!serverId) {
await reply("Команды работают только внутри сервера.");
+21 -2
View File
@@ -135,13 +135,32 @@ export async function searchTracks(
}
const [youtube, soundcloud, localHits] = await Promise.all([
ytdlp.search(text, "youtube", limit, requestedBy).catch(() => []),
ytdlp.search(text, "soundcloud", limit, requestedBy).catch(() => []),
withDeadline(ytdlp.search(text, "youtube", limit, requestedBy), "youtube"),
withDeadline(ytdlp.search(text, "soundcloud", limit, requestedBy), "soundcloud"),
local.isEnabled() ? local.search(text, 3, requestedBy).catch(() => []) : Promise.resolve([]),
]);
return [...localHits, ...interleave(youtube, soundcloud)].slice(0, limit);
}
/**
* One slow source should not hold up the whole answer: searching several at once
* means waiting for the slowest, and behind a proxy that can be tens of seconds.
*/
const SOURCE_DEADLINE_MS = 12_000;
async function withDeadline(search: Promise<Track[]>, source: string): Promise<Track[]> {
let timer: NodeJS.Timeout | undefined;
const deadline = new Promise<Track[]>((resolve) => {
timer = setTimeout(() => {
log.warn({ source }, "search took too long, answering without it");
resolve([]);
}, SOURCE_DEADLINE_MS);
});
const tracks = await Promise.race([search.catch(() => []), deadline]);
if (timer) clearTimeout(timer);
return tracks;
}
/**
* YouTube silently returns nothing for queries its restricted mode dislikes —
* same query, same words, results in a browser but an empty list over the API.