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:
parent
f93f6aafc8
commit
102cec3b56
12 changed files with 440 additions and 193 deletions
|
|
@ -104,7 +104,7 @@ public class BotInitializer {
|
||||||
var builder =
|
var builder =
|
||||||
JdaFactory.createBuilder(
|
JdaFactory.createBuilder(
|
||||||
config.getMusicToken(),
|
config.getMusicToken(),
|
||||||
"🎵 q!play",
|
"🎵 m!play",
|
||||||
GatewayIntent.GUILD_MESSAGES,
|
GatewayIntent.GUILD_MESSAGES,
|
||||||
GatewayIntent.MESSAGE_CONTENT,
|
GatewayIntent.MESSAGE_CONTENT,
|
||||||
GatewayIntent.GUILD_VOICE_STATES);
|
GatewayIntent.GUILD_VOICE_STATES);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
|
||||||
import creator.loki.feature.level.service.VoiceTracker;
|
import creator.loki.feature.level.service.VoiceTracker;
|
||||||
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
|
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
|
||||||
import creator.loki.feature.moderation.log.ModerationLogger;
|
import creator.loki.feature.moderation.log.ModerationLogger;
|
||||||
|
import creator.loki.feature.music.listener.MusicSearchButtonHandler;
|
||||||
import creator.loki.feature.music.MusicSlashCommands;
|
import creator.loki.feature.music.MusicSlashCommands;
|
||||||
import creator.loki.feature.roast.AndrertListener;
|
import creator.loki.feature.roast.AndrertListener;
|
||||||
import creator.loki.feature.roast.HelperListener;
|
import creator.loki.feature.roast.HelperListener;
|
||||||
|
|
@ -42,6 +43,7 @@ public class ListenerAggregator {
|
||||||
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
||||||
LeaderboardButtonListener leaderboardButtonListener,
|
LeaderboardButtonListener leaderboardButtonListener,
|
||||||
MessageCache messageCache,
|
MessageCache messageCache,
|
||||||
|
MusicSearchButtonHandler musicSearchButtonHandler,
|
||||||
MusicSlashCommands musicSlashCommands,
|
MusicSlashCommands musicSlashCommands,
|
||||||
FishMessageListener fishMessageListener,
|
FishMessageListener fishMessageListener,
|
||||||
AndrertListener andrertListener,
|
AndrertListener andrertListener,
|
||||||
|
|
@ -64,6 +66,7 @@ public class ListenerAggregator {
|
||||||
resetLevelsButtonHandler,
|
resetLevelsButtonHandler,
|
||||||
leaderboardButtonListener,
|
leaderboardButtonListener,
|
||||||
messageCache,
|
messageCache,
|
||||||
|
musicSearchButtonHandler,
|
||||||
musicSlashCommands,
|
musicSlashCommands,
|
||||||
fishMessageListener,
|
fishMessageListener,
|
||||||
andrertListener,
|
andrertListener,
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,24 @@ import creator.loki.feature.music.commands.PlayerCommands;
|
||||||
import creator.loki.feature.music.commands.QueueCommands;
|
import creator.loki.feature.music.commands.QueueCommands;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
|
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
||||||
import creator.loki.feature.music.play.PlayCommand;
|
import creator.loki.feature.music.play.PlayCommand;
|
||||||
|
|
||||||
public class MusicCommandRegistrar implements CommandRegistrar {
|
public class MusicCommandRegistrar implements CommandRegistrar {
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
private final SearchManager searchManager;
|
private final SearchManager searchManager;
|
||||||
|
private final MusicSearchSessionManager sessionManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager) {
|
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
|
||||||
this.playerManager = playerManager;
|
this.playerManager = playerManager;
|
||||||
this.searchManager = searchManager;
|
this.searchManager = searchManager;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void register(CommandRegistry registry) {
|
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.skip(playerManager));
|
||||||
registry.register(PlayerCommands.stop(playerManager));
|
registry.register(PlayerCommands.stop(playerManager));
|
||||||
registry.register(PlayerCommands.leave(playerManager));
|
registry.register(PlayerCommands.leave(playerManager));
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import com.google.inject.multibindings.Multibinder;
|
||||||
import creator.loki.core.registry.CommandRegistrar;
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
|
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
||||||
|
|
||||||
public class MusicModule extends AbstractModule {
|
public class MusicModule extends AbstractModule {
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(LavalinkManager.class).in(Singleton.class);
|
bind(LavalinkManager.class).in(Singleton.class);
|
||||||
bind(PlayerManager.class).in(Singleton.class);
|
bind(PlayerManager.class).in(Singleton.class);
|
||||||
|
bind(MusicSearchSessionManager.class).in(Singleton.class);
|
||||||
|
|
||||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
.addBinding()
|
.addBinding()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MusicBotListener extends ListenerAdapter {
|
public class MusicBotListener extends ListenerAdapter {
|
||||||
private static final String PREFIX = "q!";
|
private static final String PREFIX = "m!";
|
||||||
|
|
||||||
private final MusicBotPlayHandler playHandler;
|
private final MusicBotPlayHandler playHandler;
|
||||||
private final MusicBotControlHandler controlHandler;
|
private final MusicBotControlHandler controlHandler;
|
||||||
|
|
@ -72,19 +72,19 @@ public class MusicBotListener extends ListenerAdapter {
|
||||||
.setTitle("🎵 Music Bot")
|
.setTitle("🎵 Music Bot")
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"""
|
"""
|
||||||
`q!play <запрос/URL>` — Искать и играть
|
`m!play <запрос/URL>` — Искать и играть
|
||||||
`q!skip` — Пропустить
|
`m!skip` — Пропустить
|
||||||
`q!stop` — Остановить
|
`m!stop` — Остановить
|
||||||
`q!queue` — Очередь
|
`m!queue` — Очередь
|
||||||
`q!nowplaying` — Что играет
|
`m!nowplaying` — Что играет
|
||||||
`q!volume [0-100]` — Громкость
|
`m!volume [0-100]` — Громкость
|
||||||
`q!shuffle` — Перемешать
|
`m!shuffle` — Перемешать
|
||||||
`q!repeat` — Повтор
|
`m!repeat` — Повтор
|
||||||
`q!bassboost [-25..25]` — Басс
|
`m!bassboost [-25..25]` — Басс
|
||||||
`q!nightcore` — Nightcore ⚡
|
`m!nightcore` — Nightcore ⚡
|
||||||
`q!vaporwave` — Vaporwave 🌊
|
`m!vaporwave` — Vaporwave 🌊
|
||||||
`q!equalizer <band> <gain>` — Эквалайзер
|
`m!equalizer <band> <gain>` — Эквалайзер
|
||||||
`q!reset` — Сбросить фильтры
|
`m!reset` — Сбросить фильтры
|
||||||
""")
|
""")
|
||||||
.setFooter("Jockie Music style", null)
|
.setFooter("Jockie Music style", null)
|
||||||
.build())
|
.build())
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,37 @@
|
||||||
package creator.loki.feature.music.play;
|
package creator.loki.feature.music.play;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
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.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
|
||||||
import creator.loki.util.VoiceHelper;
|
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.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.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
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 {
|
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 PlayerManager playerManager;
|
||||||
private final SearchHandler searchHandler;
|
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.playerManager = playerManager;
|
||||||
this.searchHandler = new SearchHandler(searchManager);
|
this.searchHandler = new SearchHandler(searchManager);
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -37,7 +46,7 @@ public class PlayCommand implements Command {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsage() {
|
public String getUsage() {
|
||||||
return "play [yt|sc|list] <запрос> | play <номер>";
|
return "play <URL> | play <номер> | play <запрос>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -50,10 +59,7 @@ public class PlayCommand implements Command {
|
||||||
Guild guild = ctx.getGuild();
|
Guild guild = ctx.getGuild();
|
||||||
if (guild == null) return;
|
if (guild == null) return;
|
||||||
Member member = ctx.getAuthor();
|
Member member = ctx.getAuthor();
|
||||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
if (member == null) return;
|
||||||
sendErr(ctx, "Зайди в голосовой канал сначала.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var args = ctx.getArgs();
|
var args = ctx.getArgs();
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
||||||
|
|
@ -64,10 +70,86 @@ public class PlayCommand implements Command {
|
||||||
handlePickResult(ctx, first);
|
handlePickResult(ctx, first);
|
||||||
return;
|
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;
|
if (!resolveVoiceConnection(ctx)) return;
|
||||||
var lm = playerManager.getLavalinkManager();
|
var lm = playerManager.getLavalinkManager();
|
||||||
var mm = lm.getMusicManager(guild.getIdLong());
|
long guildId = ctx.getGuild().getIdLong();
|
||||||
handlePlayQuery(ctx, args, first, lm, mm);
|
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) {
|
private boolean resolveVoiceConnection(CommandContext ctx) {
|
||||||
|
|
@ -92,48 +174,6 @@ public class PlayCommand implements Command {
|
||||||
enqueueAndNotify(ctx, t);
|
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) {
|
private void enqueueAndNotify(CommandContext ctx, Track track) {
|
||||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||||
m.getScheduler().enqueue(track);
|
m.getScheduler().enqueue(track);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
|
||||||
public class SearchHandler {
|
public class SearchHandler {
|
||||||
private static final int SHOW_RESULTS = 5;
|
private static final int SHOW_RESULTS = 10;
|
||||||
|
|
||||||
private final SearchManager searchManager;
|
private final SearchManager searchManager;
|
||||||
|
|
||||||
|
|
@ -31,7 +31,8 @@ public class SearchHandler {
|
||||||
searchManager.clear(guildId);
|
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());
|
int show = Math.min(SHOW_RESULTS, tracks.size());
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < show; i++) {
|
for (int i = 0; i < show; i++) {
|
||||||
|
|
@ -44,10 +45,16 @@ public class SearchHandler {
|
||||||
.append(formatDuration(t.getInfo().getLength()))
|
.append(formatDuration(t.getInfo().getLength()))
|
||||||
.append("\n");
|
.append("\n");
|
||||||
}
|
}
|
||||||
|
String pickHint = "Выбери `" + prefix + commandName + " 1`–`" + show + "`";
|
||||||
|
String sourceLabel = switch (source) {
|
||||||
|
case "sc" -> "SoundCloud";
|
||||||
|
case "ytm" -> "YouTube Music";
|
||||||
|
default -> "YouTube";
|
||||||
|
};
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(0x00AAFF)
|
.setColor(0x00AAFF)
|
||||||
.setTitle("Результаты поиска: " + query)
|
.setTitle("Результаты поиска: " + query + " на " + sourceLabel)
|
||||||
.setDescription(sb + "\nВыбери `!" + "play" + " 1`–`" + show + "`")
|
.setDescription(sb + "\n" + pickHint)
|
||||||
.setFooter("Результаты доступны 2 минуты");
|
.setFooter("Результаты доступны 2 минуты");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +70,7 @@ public class SearchHandler {
|
||||||
return s.replace("*", "\\*").replace("_", "\\_").replace("~", "\\~");
|
return s.replace("*", "\\*").replace("_", "\\_").replace("~", "\\~");
|
||||||
}
|
}
|
||||||
|
|
||||||
static String formatDuration(long millis) {
|
public static String formatDuration(long millis) {
|
||||||
long totalSec = millis / 1000;
|
long totalSec = millis / 1000;
|
||||||
long hours = totalSec / 3600;
|
long hours = totalSec / 3600;
|
||||||
long minutes = (totalSec % 3600) / 60;
|
long minutes = (totalSec % 3600) / 60;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import creator.loki.feature.moderation.repository.*;
|
||||||
import creator.loki.feature.music.MusicCommandRegistrar;
|
import creator.loki.feature.music.MusicCommandRegistrar;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
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.profile.service.ProfileService;
|
||||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||||
import creator.loki.feature.reputation.repository.ReputationRepository;
|
import creator.loki.feature.reputation.repository.ReputationRepository;
|
||||||
|
|
@ -135,7 +136,7 @@ class SmokeCommandRegistrationTest {
|
||||||
utilityRegistrar.register(registry);
|
utilityRegistrar.register(registry);
|
||||||
|
|
||||||
var musicRegistrar =
|
var musicRegistrar =
|
||||||
new MusicCommandRegistrar(mock(PlayerManager.class), mock(SearchManager.class));
|
new MusicCommandRegistrar(mock(PlayerManager.class), mock(SearchManager.class), mock(MusicSearchSessionManager.class));
|
||||||
musicRegistrar.register(registry);
|
musicRegistrar.register(registry);
|
||||||
|
|
||||||
var economyRegistrar =
|
var economyRegistrar =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue