From 102cec3b56c483f307dbd87b2d7c8d053a62eb24 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Tue, 14 Jul 2026 15:44:13 +0200 Subject: [PATCH] feat: interactive music search with platform buttons (YT/SC/YTM) Redesign !play command: - !play -> platform selection embed with buttons [YT] [SC] [YTM] - Click platform -> search results with 10 numbered buttons - Click number -> connect to voice + play track - !play -> direct play (no buttons) - !play -> pick from cache (backward compat) Music bot prefix changed: q! -> m! Removed old PlayLoadHandler, replaced by button flow --- .../creator/loki/core/BotInitializer.java | 2 +- .../creator/loki/core/ListenerAggregator.java | 3 + .../feature/music/MusicCommandRegistrar.java | 7 +- .../loki/feature/music/MusicModule.java | 2 + .../music/listener/MusicBotListener.java | 28 +- .../listener/MusicSearchButtonHandler.java | 242 ++++++++++++++++++ .../music/play/MusicSearchSession.java | 38 +++ .../music/play/MusicSearchSessionManager.java | 27 ++ .../loki/feature/music/play/PlayCommand.java | 148 +++++++---- .../feature/music/play/PlayLoadHandler.java | 116 --------- .../feature/music/play/SearchHandler.java | 17 +- .../SmokeCommandRegistrationTest.java | 3 +- 12 files changed, 440 insertions(+), 193 deletions(-) create mode 100644 src/main/java/creator/loki/feature/music/listener/MusicSearchButtonHandler.java create mode 100644 src/main/java/creator/loki/feature/music/play/MusicSearchSession.java create mode 100644 src/main/java/creator/loki/feature/music/play/MusicSearchSessionManager.java delete mode 100644 src/main/java/creator/loki/feature/music/play/PlayLoadHandler.java diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java index b417f8e..44f360a 100644 --- a/src/main/java/creator/loki/core/BotInitializer.java +++ b/src/main/java/creator/loki/core/BotInitializer.java @@ -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); diff --git a/src/main/java/creator/loki/core/ListenerAggregator.java b/src/main/java/creator/loki/core/ListenerAggregator.java index 31775cb..6e861b7 100644 --- a/src/main/java/creator/loki/core/ListenerAggregator.java +++ b/src/main/java/creator/loki/core/ListenerAggregator.java @@ -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, diff --git a/src/main/java/creator/loki/feature/music/MusicCommandRegistrar.java b/src/main/java/creator/loki/feature/music/MusicCommandRegistrar.java index 15ef8f2..32b97d4 100644 --- a/src/main/java/creator/loki/feature/music/MusicCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/music/MusicCommandRegistrar.java @@ -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)); diff --git a/src/main/java/creator/loki/feature/music/MusicModule.java b/src/main/java/creator/loki/feature/music/MusicModule.java index 5f6aee6..692dacc 100644 --- a/src/main/java/creator/loki/feature/music/MusicModule.java +++ b/src/main/java/creator/loki/feature/music/MusicModule.java @@ -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() diff --git a/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java b/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java index 417bf3e..d78cdee 100644 --- a/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java +++ b/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java @@ -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 ` — Эквалайзер - `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 ` — Эквалайзер + `m!reset` — Сбросить фильтры """) .setFooter("Jockie Music style", null) .build()) diff --git a/src/main/java/creator/loki/feature/music/listener/MusicSearchButtonHandler.java b/src/main/java/creator/loki/feature/music/listener/MusicSearchButtonHandler.java new file mode 100644 index 0000000..ceccfea --- /dev/null +++ b/src/main/java/creator/loki/feature/music/listener/MusicSearchButtonHandler.java @@ -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 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 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 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 buildNumberButtons(int count) { + int rows = (count + 4) / 5; + var list = new java.util.ArrayList(); + for (int r = 0; r < rows; r++) { + var builder = new java.util.ArrayList