feat: interactive music search with platform buttons (YT/SC/YTM)

Redesign !play command:
- !play <query> -> platform selection embed with buttons [YT] [SC] [YTM]
- Click platform -> search results with 10 numbered buttons
- Click number -> connect to voice + play track
- !play <URL> -> direct play (no buttons)
- !play <number> -> pick from cache (backward compat)

Music bot prefix changed: q! -> m!
Removed old PlayLoadHandler, replaced by button flow
This commit is contained in:
loki5512344 2026-07-14 15:44:13 +02:00
parent f93f6aafc8
commit 102cec3b56
Signed by: boba
GPG key ID: 253067914055423B
12 changed files with 440 additions and 193 deletions

View file

@ -104,7 +104,7 @@ public class BotInitializer {
var builder =
JdaFactory.createBuilder(
config.getMusicToken(),
"🎵 q!play",
"🎵 m!play",
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_VOICE_STATES);

View file

@ -13,6 +13,7 @@ import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
import creator.loki.feature.level.service.VoiceTracker;
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
import creator.loki.feature.moderation.log.ModerationLogger;
import creator.loki.feature.music.listener.MusicSearchButtonHandler;
import creator.loki.feature.music.MusicSlashCommands;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.roast.HelperListener;
@ -42,6 +43,7 @@ public class ListenerAggregator {
ResetLevelsButtonHandler resetLevelsButtonHandler,
LeaderboardButtonListener leaderboardButtonListener,
MessageCache messageCache,
MusicSearchButtonHandler musicSearchButtonHandler,
MusicSlashCommands musicSlashCommands,
FishMessageListener fishMessageListener,
AndrertListener andrertListener,
@ -64,6 +66,7 @@ public class ListenerAggregator {
resetLevelsButtonHandler,
leaderboardButtonListener,
messageCache,
musicSearchButtonHandler,
musicSlashCommands,
fishMessageListener,
andrertListener,

View file

@ -8,21 +8,24 @@ import creator.loki.feature.music.commands.PlayerCommands;
import creator.loki.feature.music.commands.QueueCommands;
import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.music.play.MusicSearchSessionManager;
import creator.loki.feature.music.play.PlayCommand;
public class MusicCommandRegistrar implements CommandRegistrar {
private final PlayerManager playerManager;
private final SearchManager searchManager;
private final MusicSearchSessionManager sessionManager;
@Inject
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager) {
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
this.playerManager = playerManager;
this.searchManager = searchManager;
this.sessionManager = sessionManager;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new PlayCommand(playerManager, searchManager));
registry.register(new PlayCommand(playerManager, searchManager, sessionManager));
registry.register(PlayerCommands.skip(playerManager));
registry.register(PlayerCommands.stop(playerManager));
registry.register(PlayerCommands.leave(playerManager));

View file

@ -6,12 +6,14 @@ import com.google.inject.multibindings.Multibinder;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.play.MusicSearchSessionManager;
public class MusicModule extends AbstractModule {
@Override
protected void configure() {
bind(LavalinkManager.class).in(Singleton.class);
bind(PlayerManager.class).in(Singleton.class);
bind(MusicSearchSessionManager.class).in(Singleton.class);
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
.addBinding()

View file

@ -13,7 +13,7 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
public class MusicBotListener extends ListenerAdapter {
private static final String PREFIX = "q!";
private static final String PREFIX = "m!";
private final MusicBotPlayHandler playHandler;
private final MusicBotControlHandler controlHandler;
@ -72,19 +72,19 @@ public class MusicBotListener extends ListenerAdapter {
.setTitle("🎵 Music Bot")
.setDescription(
"""
`q!play <запрос/URL>` — Искать и играть
`q!skip` — Пропустить
`q!stop` — Остановить
`q!queue` — Очередь
`q!nowplaying` — Что играет
`q!volume [0-100]` — Громкость
`q!shuffle` — Перемешать
`q!repeat` — Повтор
`q!bassboost [-25..25]` — Басс
`q!nightcore` — Nightcore ⚡
`q!vaporwave` — Vaporwave 🌊
`q!equalizer <band> <gain>` — Эквалайзер
`q!reset` — Сбросить фильтры
`m!play <запрос/URL>` — Искать и играть
`m!skip` — Пропустить
`m!stop` — Остановить
`m!queue` — Очередь
`m!nowplaying` — Что играет
`m!volume [0-100]` — Громкость
`m!shuffle` — Перемешать
`m!repeat` — Повтор
`m!bassboost [-25..25]` — Басс
`m!nightcore` — Nightcore ⚡
`m!vaporwave` — Vaporwave 🌊
`m!equalizer <band> <gain>` — Эквалайзер
`m!reset` — Сбросить фильтры
""")
.setFooter("Jockie Music style", null)
.build())

View file

@ -0,0 +1,242 @@
package creator.loki.feature.music.listener;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.music.manage.GuildMusicManager;
import creator.loki.feature.music.play.MusicSearchSession;
import creator.loki.feature.music.play.MusicSearchSessionManager;
import creator.loki.feature.music.play.SearchHandler;
import creator.loki.util.VoiceHelper;
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.player.LoadFailed;
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
import dev.arbjerg.lavalink.client.player.SearchResult;
import dev.arbjerg.lavalink.client.player.Track;
import dev.arbjerg.lavalink.client.player.TrackLoaded;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
@Singleton
public class MusicSearchButtonHandler extends ListenerAdapter {
private static final Color BRAND = new Color(88, 101, 242);
private static final int MAX_RESULTS = 10;
private final MusicSearchSessionManager sessionManager;
private final LavalinkManager lavalinkManager;
private final SearchHandler searchHandler;
@Inject
public MusicSearchButtonHandler(
MusicSearchSessionManager sessionManager,
LavalinkManager lavalinkManager,
SearchManager searchManager) {
this.sessionManager = sessionManager;
this.lavalinkManager = lavalinkManager;
this.searchHandler = new SearchHandler(searchManager);
}
@Override
public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
String id = event.getComponentId();
if (id.startsWith("ms_platform_")) {
handlePlatform(event, id.replace("ms_platform_", ""));
} else if (id.startsWith("ms_pick_")) {
handlePick(event, id.replace("ms_pick_", ""));
}
}
private void handlePlatform(ButtonInteractionEvent event, String source) {
String messageId = event.getMessageId();
MusicSearchSession session = sessionManager.get(messageId);
if (session == null) {
event
.replyEmbeds(
EmbedTemplates.error(
"Поиск устарел", "Сделай новый поиск: `" + getPrefix() + "play <запрос>`"))
.setEphemeral(true)
.queue();
return;
}
event.deferEdit().queue();
String query = session.getQuery();
String searchId = switch (source) {
case "sc" -> "scsearch:" + query;
case "ytm" -> "ytmsearch:" + query;
default -> "ytsearch:" + query;
};
event.getHook()
.editOriginalEmbeds(
new EmbedBuilder()
.setColor(BRAND)
.setTitle("🔍 Ищу: " + query)
.setDescription("Поиск на " + sourceLabel(source) + "...")
.build())
.setComponents()
.queue();
lavalinkManager
.getOrCreateLink(session.getGuildId())
.loadItem(searchId)
.subscribe(
new AbstractAudioLoadResultHandler() {
@Override
public void onSearchResultLoaded(SearchResult result) {
if (result.getTracks().isEmpty()) {
noMatch();
return;
}
showResults(event, result.getTracks(), query, source);
}
@Override
public void ontrackLoaded(TrackLoaded result) {
showResults(event, List.of(result.getTrack()), query, source);
}
@Override
public void onPlaylistLoaded(PlaylistLoaded result) {
showResults(event, result.getTracks(), query, source);
}
@Override
public void noMatches() {
noMatch();
}
@Override
public void loadFailed(LoadFailed e) {
String msg =
e.getException().getMessage() != null
? e.getException().getMessage()
: "Неизвестная ошибка";
event
.getHook()
.editOriginalEmbeds(EmbedTemplates.error("Ошибка загрузки", msg))
.queue();
}
private void noMatch() {
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.error("Не найдено", "Ничего не найдено: " + query))
.queue();
}
});
}
private void showResults(
ButtonInteractionEvent event, List<Track> tracks, String query, String source) {
String messageId = event.getMessageId();
MusicSearchSession session = sessionManager.get(messageId);
if (session == null) return;
int show = Math.min(MAX_RESULTS, tracks.size());
List<Track> display = tracks.subList(0, show);
session.setResults(tracks);
EmbedBuilder embed = searchHandler.buildResultsEmbed(
query, display, source, getPrefix(), "play");
var rows = buildNumberButtons(show);
event.getHook().editOriginalEmbeds(embed.build()).setComponents(rows).queue();
}
private void handlePick(ButtonInteractionEvent event, String indexStr) {
int index = Integer.parseInt(indexStr);
String messageId = event.getMessageId();
MusicSearchSession session = sessionManager.get(messageId);
if (session == null) {
event
.replyEmbeds(
EmbedTemplates.error(
"Поиск устарел", "Сделай новый поиск: `" + getPrefix() + "play <запрос>`"))
.setEphemeral(true)
.queue();
return;
}
List<Track> tracks = session.getResults();
if (tracks == null || index < 0 || index >= tracks.size()) {
event
.replyEmbeds(EmbedTemplates.error("Ошибка", "Результаты поиска устарели."))
.setEphemeral(true)
.queue();
return;
}
Track track = tracks.get(index);
Member member = event.getMember();
Guild guild = event.getGuild();
if (member == null || !member.getVoiceState().inAudioChannel()) {
event
.replyEmbeds(
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
.setEphemeral(true)
.queue();
return;
}
event.deferEdit().queue();
VoiceHelper.connectForPlayback(guild, member, lavalinkManager);
GuildMusicManager mm = lavalinkManager.getMusicManager(session.getGuildId());
mm.getScheduler().enqueue(track);
String duration = SearchHandler.formatDuration(track.getInfo().getLength());
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.info(
"Добавлено в очередь",
"**" + track.getInfo().getTitle() + "**\n" + duration))
.setComponents()
.queue();
sessionManager.remove(messageId);
}
private static List<ActionRow> buildNumberButtons(int count) {
int rows = (count + 4) / 5;
var list = new java.util.ArrayList<ActionRow>();
for (int r = 0; r < rows; r++) {
var builder = new java.util.ArrayList<Button>();
for (int c = 0; c < 5; c++) {
int idx = r * 5 + c;
if (idx >= count) break;
builder.add(Button.primary("ms_pick_" + idx, String.valueOf(idx + 1)));
}
list.add(ActionRow.of(builder));
}
return list;
}
private static String sourceLabel(String source) {
return switch (source) {
case "sc" -> "SoundCloud";
case "ytm" -> "YouTube Music";
default -> "YouTube";
};
}
private String getPrefix() {
return lavalinkManager.getConfig().getPrefix();
}
}

View file

@ -0,0 +1,38 @@
package creator.loki.feature.music.play;
import java.util.List;
import dev.arbjerg.lavalink.client.player.Track;
public class MusicSearchSession {
private final long guildId;
private final long userId;
private final String query;
private List<Track> results;
public MusicSearchSession(long guildId, long userId, String query) {
this.guildId = guildId;
this.userId = userId;
this.query = query;
}
public long getGuildId() {
return guildId;
}
public long getUserId() {
return userId;
}
public String getQuery() {
return query;
}
public List<Track> getResults() {
return results;
}
public void setResults(List<Track> results) {
this.results = results;
}
}

View file

@ -0,0 +1,27 @@
package creator.loki.feature.music.play;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class MusicSearchSessionManager {
private final Cache<String, MusicSearchSession> sessions;
public MusicSearchSessionManager() {
this.sessions =
Caffeine.newBuilder().maximumSize(100).expireAfterWrite(3, TimeUnit.MINUTES).build();
}
public void put(String messageId, MusicSearchSession session) {
sessions.put(messageId, session);
}
public MusicSearchSession get(String messageId) {
return sessions.getIfPresent(messageId);
}
public void remove(String messageId) {
sessions.invalidate(messageId);
}
}

View file

@ -1,28 +1,37 @@
package creator.loki.feature.music.play;
import java.awt.Color;
import java.util.List;
import java.util.Set;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.music.manage.GuildMusicManager;
import creator.loki.util.VoiceHelper;
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.player.LoadFailed;
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
import dev.arbjerg.lavalink.client.player.SearchResult;
import dev.arbjerg.lavalink.client.player.Track;
import dev.arbjerg.lavalink.client.player.TrackLoaded;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
public class PlayCommand implements Command {
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
private static final Color BRAND = new Color(88, 101, 242);
private final PlayerManager playerManager;
private final SearchHandler searchHandler;
private final MusicSearchSessionManager sessionManager;
public PlayCommand(PlayerManager playerManager, SearchManager searchManager) {
public PlayCommand(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
this.playerManager = playerManager;
this.searchHandler = new SearchHandler(searchManager);
this.sessionManager = sessionManager;
}
@Override
@ -37,7 +46,7 @@ public class PlayCommand implements Command {
@Override
public String getUsage() {
return "play [yt|sc|list] <запрос> | play <номер>";
return "play <URL> | play <номер> | play <запрос>";
}
@Override
@ -50,10 +59,7 @@ public class PlayCommand implements Command {
Guild guild = ctx.getGuild();
if (guild == null) return;
Member member = ctx.getAuthor();
if (member == null || !member.getVoiceState().inAudioChannel()) {
sendErr(ctx, "Зайди в голосовой канал сначала.");
return;
}
if (member == null) return;
var args = ctx.getArgs();
if (args.isEmpty()) {
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
@ -64,10 +70,86 @@ public class PlayCommand implements Command {
handlePickResult(ctx, first);
return;
}
if (first.startsWith("http://") || first.startsWith("https://")) {
handleUrl(ctx, first);
return;
}
showPlatformSelection(ctx, first);
}
private void showPlatformSelection(CommandContext ctx, String query) {
Guild guild = ctx.getGuild();
Member member = ctx.getAuthor();
var lm = playerManager.getLavalinkManager();
var embed = new EmbedBuilder()
.setColor(BRAND)
.setTitle("Выбери платформу")
.setDescription("Результаты поиска для: **" + query + "**\nВыбери платформу для поиска:")
.build();
var row = ActionRow.of(
Button.primary("ms_platform_yt", "🇾 YouTube"),
Button.primary("ms_platform_sc", "🎵 SoundCloud"),
Button.primary("ms_platform_ytm", "▶️ YouTube Music"));
var message = ctx.getChannel()
.sendMessageEmbeds(embed)
.addComponents(row)
.complete();
var session = new MusicSearchSession(guild.getIdLong(), member.getIdLong(), query);
sessionManager.put(message.getId(), session);
}
private void handleUrl(CommandContext ctx, String url) {
if (!resolveVoiceConnection(ctx)) return;
var lm = playerManager.getLavalinkManager();
var mm = lm.getMusicManager(guild.getIdLong());
handlePlayQuery(ctx, args, first, lm, mm);
long guildId = ctx.getGuild().getIdLong();
lm.getOrCreateLink(guildId)
.loadItem(url)
.subscribe(new AbstractAudioLoadResultHandler() {
@Override
public void ontrackLoaded(TrackLoaded result) {
enqueueAndNotify(ctx, result.getTrack());
}
@Override
public void onSearchResultLoaded(SearchResult result) {
if (result.getTracks().isEmpty()) {
sendErr(ctx, "Ничего не найдено.");
} else {
enqueueAndNotify(ctx, result.getTracks().get(0));
}
}
@Override
public void onPlaylistLoaded(PlaylistLoaded result) {
var mm = lm.getMusicManager(guildId);
for (Track t : result.getTracks()) {
mm.getScheduler().enqueue(t);
}
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.info(
"Плейлист добавлен",
"**" + result.getInfo().getName() + "** — " + result.getTracks().size() + " треков"))
.queue();
}
@Override
public void noMatches() {
sendErr(ctx, "Ничего не найдено.");
}
@Override
public void loadFailed(LoadFailed e) {
String msg = e.getException().getMessage() != null
? e.getException().getMessage()
: "Ошибка загрузки";
sendErr(ctx, msg);
}
});
}
private boolean resolveVoiceConnection(CommandContext ctx) {
@ -92,48 +174,6 @@ public class PlayCommand implements Command {
enqueueAndNotify(ctx, t);
}
private void handlePlayQuery(
CommandContext ctx,
List<String> args,
String first,
LavalinkManager lm,
GuildMusicManager mm) {
boolean isSource = SOURCES.contains(first);
String raw = String.join(" ", isSource ? args.subList(1, args.size()) : args);
if (raw.isEmpty()) {
sendErr(ctx, "Укажи запрос после источника.");
return;
}
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
String query = isUrl ? null : raw;
String id = buildQuery(first, raw);
loadAndHandle(ctx, lm, ctx.getGuild().getIdLong(), mm, id, isUrl, query, raw);
}
private void loadAndHandle(
CommandContext ctx,
LavalinkManager lm,
long guildId,
GuildMusicManager mm,
String id,
boolean isUrl,
String query,
String raw) {
lm.getOrCreateLink(guildId)
.loadItem(id)
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, query, raw));
}
private static String buildQuery(String first, String raw) {
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
if (!first.equals("list")
&& !raw.startsWith("ytsearch:")
&& !raw.startsWith("ytmsearch:")
&& !raw.startsWith("scsearch:")) return "ytsearch:" + raw;
if (first.equals("list")) return "ytsearch:" + raw;
return raw;
}
private void enqueueAndNotify(CommandContext ctx, Track track) {
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
m.getScheduler().enqueue(track);

View file

@ -1,116 +0,0 @@
package creator.loki.feature.music.play;
import java.util.List;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.music.manage.GuildMusicManager;
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
import dev.arbjerg.lavalink.client.player.LoadFailed;
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
import dev.arbjerg.lavalink.client.player.SearchResult;
import dev.arbjerg.lavalink.client.player.Track;
import dev.arbjerg.lavalink.client.player.TrackLoaded;
class PlayLoadHandler extends AbstractAudioLoadResultHandler {
private final CommandContext ctx;
private final SearchHandler searchHandler;
private final GuildMusicManager mm;
private final boolean isUrl;
private final String query;
private final String raw;
PlayLoadHandler(
CommandContext ctx,
SearchHandler searchHandler,
GuildMusicManager mm,
boolean isUrl,
String query,
String raw) {
this.ctx = ctx;
this.searchHandler = searchHandler;
this.mm = mm;
this.isUrl = isUrl;
this.query = query;
this.raw = raw;
}
@Override
public void ontrackLoaded(TrackLoaded r) {
if (isUrl) enqueueAndNotify(r.getTrack());
else showResults(List.of(r.getTrack()));
}
@Override
public void onPlaylistLoaded(PlaylistLoaded r) {
if (isUrl) {
int count = 0;
for (Track t : r.getTracks()) {
mm.getScheduler().enqueue(t);
count++;
}
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.info(
"Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + count + " треков"))
.queue();
} else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
enqueueAndNotify(r.getTracks().get(0));
} else {
showResults(r.getTracks());
}
}
@Override
public void onSearchResultLoaded(SearchResult r) {
String noMatch =
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
if (r.getTracks().isEmpty()) {
sendErr(noMatch);
} else if (isUrl) {
enqueueAndNotify(r.getTracks().get(0));
} else {
showResults(r.getTracks());
}
}
@Override
public void noMatches() {
String noMatch =
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
sendErr(noMatch);
}
@Override
public void loadFailed(LoadFailed e) {
sendErr(
e.getException().getMessage() != null
? e.getException().getMessage()
: "Неизвестная ошибка");
}
private void enqueueAndNotify(Track track) {
mm.getScheduler().enqueue(track);
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.info(
"Добавлено в очередь",
"**"
+ track.getInfo().getTitle()
+ "**\n"
+ SearchHandler.formatDuration(track.getInfo().getLength())))
.queue();
}
private void showResults(List<Track> list) {
if (query == null) return;
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
ctx.getChannel()
.sendMessageEmbeds(searchHandler.buildResultsEmbed(query, list).build())
.queue();
}
private void sendErr(String msg) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
}
}

View file

@ -7,7 +7,7 @@ import dev.arbjerg.lavalink.client.player.Track;
import net.dv8tion.jda.api.EmbedBuilder;
public class SearchHandler {
private static final int SHOW_RESULTS = 5;
private static final int SHOW_RESULTS = 10;
private final SearchManager searchManager;
@ -31,7 +31,8 @@ public class SearchHandler {
searchManager.clear(guildId);
}
public EmbedBuilder buildResultsEmbed(String query, List<Track> tracks) {
public EmbedBuilder buildResultsEmbed(
String query, List<Track> tracks, String source, String prefix, String commandName) {
int show = Math.min(SHOW_RESULTS, tracks.size());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < show; i++) {
@ -44,10 +45,16 @@ public class SearchHandler {
.append(formatDuration(t.getInfo().getLength()))
.append("\n");
}
String pickHint = "Выбери `" + prefix + commandName + " 1`–`" + show + "`";
String sourceLabel = switch (source) {
case "sc" -> "SoundCloud";
case "ytm" -> "YouTube Music";
default -> "YouTube";
};
return new EmbedBuilder()
.setColor(0x00AAFF)
.setTitle("Результаты поиска: " + query)
.setDescription(sb + "\nВыбери `!" + "play" + " 1`–`" + show + "`")
.setTitle("Результаты поиска: " + query + " на " + sourceLabel)
.setDescription(sb + "\n" + pickHint)
.setFooter("Результаты доступны 2 минуты");
}
@ -63,7 +70,7 @@ public class SearchHandler {
return s.replace("*", "\\*").replace("_", "\\_").replace("~", "\\~");
}
static String formatDuration(long millis) {
public static String formatDuration(long millis) {
long totalSec = millis / 1000;
long hours = totalSec / 3600;
long minutes = (totalSec % 3600) / 60;

View file

@ -29,6 +29,7 @@ import creator.loki.feature.moderation.repository.*;
import creator.loki.feature.music.MusicCommandRegistrar;
import creator.loki.feature.music.core.PlayerManager;
import creator.loki.feature.music.core.SearchManager;
import creator.loki.feature.music.play.MusicSearchSessionManager;
import creator.loki.feature.profile.service.ProfileService;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.feature.reputation.repository.ReputationRepository;
@ -135,7 +136,7 @@ class SmokeCommandRegistrationTest {
utilityRegistrar.register(registry);
var musicRegistrar =
new MusicCommandRegistrar(mock(PlayerManager.class), mock(SearchManager.class));
new MusicCommandRegistrar(mock(PlayerManager.class), mock(SearchManager.class), mock(MusicSearchSessionManager.class));
musicRegistrar.register(registry);
var economyRegistrar =