From 108f63f7ec7ebf43a0fe3816600c283e157117ee Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 3 Jul 2026 01:24:36 +0200 Subject: [PATCH] fix: music system improvements, radio refactoring, lavalink config --- docker-compose.yml | 2 +- lavalink/application.yml | 16 +- .../feature/music/core/LavalinkManager.java | 2 +- .../music/listener/MusicBotPlayHandler.java | 10 +- .../loki/feature/music/play/PlayCommand.java | 27 ++- .../feature/music/slash/PlaySlashHandler.java | 2 +- .../loki/feature/radio/RadioManager.java | 173 +++++++----------- .../libraries/jda/JDAVoiceUpdateListener.java | 41 +++++ 8 files changed, 137 insertions(+), 136 deletions(-) create mode 100644 src/main/java/dev/arbjerg/lavalink/libraries/jda/JDAVoiceUpdateListener.java diff --git a/docker-compose.yml b/docker-compose.yml index 7cb45ca..748410a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - ./lavalink/plugins/:/opt/Lavalink/plugins/ - lavalink-data:/opt/Lavalink/data environment: - JAVA_OPTS: "-Xmx256M -Xms64M" + JAVA_OPTS: "-Xmx1G -Xms256M" SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} diff --git a/lavalink/application.yml b/lavalink/application.yml index 9c26da2..8e26893 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -3,6 +3,12 @@ server: address: 0.0.0.0 plugins: + youtube: + enabled: true + clients: + - MUSIC + - WEB + - ANDROID_VR lavasrc: providers: - "ytsearch:\"%ISRC%\"" @@ -23,12 +29,16 @@ plugins: mediaType: "music" playlistLoadLimit: 6 - lavalink: + plugins: + - dependency: "dev.lavalink.youtube:youtube-plugin:1.18.1" + repository: "https://maven.lavalink.dev/releases" + - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.8.1" + repository: "https://maven.topi.wtf/releases" server: password: "Hch4HiVCxkKs9JPiSnrt4Q" sources: - youtube: true + youtube: false soundcloud: true bandcamp: true twitch: true @@ -37,7 +47,7 @@ lavalink: bufferDurationMs: 400 youtubePlaylistLoadLimit: 6 playerUpdateInterval: 5 - youtubeSearchEnabled: true + youtubeSearchEnabled: false soundcloudSearchEnabled: true gc-warnings: true diff --git a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java index 719b786..0f07013 100644 --- a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java +++ b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java @@ -53,7 +53,7 @@ public class LavalinkManager { .setName("main") .setServerUri(lavalinkConfig.getWsUri()) .setPassword(lavalinkConfig.getPassword()) - .setHttpTimeout(10000L) + .setHttpTimeout(30000L) .build()); registerListeners(); diff --git a/src/main/java/creator/loki/feature/music/listener/MusicBotPlayHandler.java b/src/main/java/creator/loki/feature/music/listener/MusicBotPlayHandler.java index 0cdcfd3..56cff6a 100644 --- a/src/main/java/creator/loki/feature/music/listener/MusicBotPlayHandler.java +++ b/src/main/java/creator/loki/feature/music/listener/MusicBotPlayHandler.java @@ -91,18 +91,10 @@ public class MusicBotPlayHandler { } private static String resolveIdentifier(String query) { - if (query.startsWith("https://www.youtube.com/") - || query.startsWith("https://youtu.be/") - || query.startsWith("https://music.youtube.com/")) { - return "ytmsearch:" + query; - } - if (query.startsWith("https://open.spotify.com/")) { - return "spsearch:" + query; - } if (query.startsWith("http://") || query.startsWith("https://")) { return query; } - return "ytmsearch:" + query; + return "ytsearch:" + query; } private void connect(Guild g, Member m, LavalinkManager lm) { diff --git a/src/main/java/creator/loki/feature/music/play/PlayCommand.java b/src/main/java/creator/loki/feature/music/play/PlayCommand.java index 1913293..9895519 100644 --- a/src/main/java/creator/loki/feature/music/play/PlayCommand.java +++ b/src/main/java/creator/loki/feature/music/play/PlayCommand.java @@ -109,30 +109,29 @@ public class PlayCommand implements Command { sendErr(ctx, "Укажи запрос после источника."); return; } - String id; + boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://"); - boolean isYt = - isUrl - && (raw.contains("youtube.com/") - || raw.contains("youtu.be/") - || raw.contains("spotify.com/")); boolean isList = !isUrl && first.equals("list"); - if (isYt) { - id = "ytmsearch:" + raw; - } else if (isUrl) { + + String id; + if (isUrl) { id = raw; } else if (isList) { id = "ytsearch:" + raw; + } else if (raw.startsWith("ytmsearch:") + || raw.startsWith("ytsearch:") + || raw.startsWith("scsearch:")) { + id = raw; } else { - id = - raw.startsWith("ytmsearch:") || raw.startsWith("ytsearch:") || raw.startsWith("scsearch:") - ? raw - : "ytmsearch:" + raw; + id = "ytsearch:" + raw; } - boolean autoPlay = isUrl || (!isList); + + boolean autoPlay = isUrl; + String noMatch = isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw; String query = isList ? raw : (isUrl ? null : raw); + lm.getOrCreateLink(guild.getIdLong()) .loadItem(id) .subscribe( diff --git a/src/main/java/creator/loki/feature/music/slash/PlaySlashHandler.java b/src/main/java/creator/loki/feature/music/slash/PlaySlashHandler.java index 1ae1e16..818ec99 100644 --- a/src/main/java/creator/loki/feature/music/slash/PlaySlashHandler.java +++ b/src/main/java/creator/loki/feature/music/slash/PlaySlashHandler.java @@ -49,7 +49,7 @@ public class PlaySlashHandler { private void search( SlashCommandInteractionEvent event, String query, GuildMusicManager m, Guild guild) { - trackLoader.loadItem(m.getGuildId(), "ytmsearch:" + query, loader(event, m, guild, query)); + trackLoader.loadItem(m.getGuildId(), "ytsearch:" + query, loader(event, m, guild, query)); } private TrackLoader.LoadCallbacks loader( diff --git a/src/main/java/creator/loki/feature/radio/RadioManager.java b/src/main/java/creator/loki/feature/radio/RadioManager.java index 192f0f8..62f99bf 100644 --- a/src/main/java/creator/loki/feature/radio/RadioManager.java +++ b/src/main/java/creator/loki/feature/radio/RadioManager.java @@ -3,37 +3,29 @@ package creator.loki.feature.radio; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import com.google.inject.Singleton; -import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; -import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; -import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; -import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; -import creator.loki.feature.voice.AudioPlayerSendHandler; +import creator.loki.feature.music.core.LavalinkManager; +import creator.loki.feature.music.manage.GuildMusicManager; import creator.loki.util.LoggerDecorator; +import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; +import dev.arbjerg.lavalink.client.Link; +import dev.arbjerg.lavalink.client.player.Track; +import dev.arbjerg.lavalink.client.player.TrackLoaded; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@Singleton public class RadioManager { private static final Logger logger = LoggerFactory.getLogger(RadioManager.class); - private final AudioPlayerManager playerManager; - private final Map radioStates; + private final Map radioStates = new ConcurrentHashMap<>(); + private LavalinkManager lavalinkManager; private JDA radioJda; - public RadioManager() { - this.playerManager = new DefaultAudioPlayerManager(); - AudioSourceManagers.registerRemoteSources(playerManager); - this.radioStates = new ConcurrentHashMap<>(); + public void setLavalinkManager(LavalinkManager lm) { + this.lavalinkManager = lm; } public void setRadioJda(JDA radioJda) { @@ -53,113 +45,81 @@ public class RadioManager { public void startRadio(long guildId, long channelId) { GuildRadioState state = radioStates.get(guildId); if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) return; - if (radioJda == null) return; + if (radioJda == null || lavalinkManager == null) return; Guild guild = radioJda.getGuildById(guildId); - VoiceChannel voiceChannel = guild != null ? guild.getVoiceChannelById(channelId) : null; - if (guild == null || voiceChannel == null) return; + if (guild == null) return; - // Already playing in this channel — ignore duplicate start - if (state.player != null && guild.getAudioManager().isConnected()) return; + guild.getJDA().getDirectAudioController().connect( + guild.getVoiceChannelById(channelId)); - if (state.player != null) { - state.player.destroy(); - state.player = null; - } + var mm = new GuildMusicManager(guildId, lavalinkManager, null); + state.musicManager = mm; - AudioPlayer player = playerManager.createPlayer(); - player.addListener( - new AudioEventAdapter() { - @Override - public void onTrackEnd(AudioPlayer p, AudioTrack track, AudioTrackEndReason reason) { - if (reason == AudioTrackEndReason.FINISHED || reason == AudioTrackEndReason.STOPPED) { - p.playTrack(track.makeClone()); - } - } + lavalinkManager + .getOrCreateLink(guildId) + .loadItem(state.streamUrl) + .subscribe( + new AbstractAudioLoadResultHandler() { + @Override + public void ontrackLoaded(TrackLoaded r) { + mm.getScheduler().enqueue(r.getTrack()); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } - @Override - public void onTrackException(AudioPlayer p, AudioTrack track, FriendlyException ex) { - LoggerDecorator.warn(logger, "Radio error, restarting: " + ex.getMessage()); - p.playTrack(track.makeClone()); - } - }); - state.player = player; - state.volume = Math.max(0, Math.min(100, state.volume)); - player.setVolume(state.volume); + @Override + public void onPlaylistLoaded( + dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { + if (!r.getTracks().isEmpty()) { + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } - guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player)); - guild.getAudioManager().openAudioConnection(voiceChannel); + @Override + public void onSearchResultLoaded( + dev.arbjerg.lavalink.client.player.SearchResult r) { + if (!r.getTracks().isEmpty()) { + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } - playerManager.loadItem( - state.streamUrl, - new AudioLoadResultHandler() { - @Override - public void trackLoaded(AudioTrack track) { - player.playTrack(track); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } + @Override + public void noMatches() { + LoggerDecorator.warn(logger, "No radio stream: " + state.streamUrl); + } - @Override - public void playlistLoaded(AudioPlaylist playlist) { - AudioTrack track = playlist.getTracks().isEmpty() ? null : playlist.getTracks().get(0); - if (track != null) player.playTrack(track); - } - - @Override - public void noMatches() { - LoggerDecorator.warn(logger, "No radio stream: " + state.streamUrl); - } - - @Override - public void loadFailed(FriendlyException exception) { - LoggerDecorator.error(logger, "Failed to load radio: " + state.streamUrl, exception); - } - }); + @Override + public void loadFailed( + dev.arbjerg.lavalink.client.player.LoadFailed e) { + LoggerDecorator.warn(logger, "Failed to load radio: " + + (e.getException().getMessage() != null + ? e.getException().getMessage() : "unknown")); + } + }); } public void stopRadio(long guildId) { - GuildRadioState state = radioStates.get(guildId); + GuildRadioState state = radioStates.remove(guildId); if (state == null) return; - - if (state.player != null) { - state.player.destroy(); - state.player = null; + if (state.musicManager != null) { + state.musicManager.stop(); + state.musicManager.destroy(); + } + if (lavalinkManager != null) { + Link link = lavalinkManager.getClient().getLinkIfCached(guildId); + if (link != null) link.destroy().subscribe(); } - if (radioJda != null) { Guild guild = radioJda.getGuildById(guildId); - if (guild != null) { - guild.getAudioManager().closeAudioConnection(); - guild.getAudioManager().setSendingHandler(null); - } + if (guild != null) guild.getAudioManager().closeAudioConnection(); } - LoggerDecorator.music(logger, "Radio stopped for guild " + guildId); } - public void setVolume(long guildId, int volume) { - int vol = Math.max(0, Math.min(100, volume)); - GuildRadioState state = radioStates.get(guildId); - if (state == null) { - GuildRadioState newState = new GuildRadioState(); - newState.volume = vol; - radioStates.put(guildId, newState); - return; - } - state.volume = vol; - if (state.player != null) { - state.player.setVolume(vol); - } - } - - public int getVolume(long guildId) { - GuildRadioState state = radioStates.get(guildId); - return state != null ? state.volume : 50; - } - public boolean isRadioActive(long guildId) { - GuildRadioState state = radioStates.get(guildId); - return state != null && state.player != null; + return radioStates.containsKey(guildId); } public boolean hasRadioBot() { @@ -172,7 +132,6 @@ public class RadioManager { private static class GuildRadioState { String streamUrl; - AudioPlayer player; - int volume = 50; + GuildMusicManager musicManager; } } diff --git a/src/main/java/dev/arbjerg/lavalink/libraries/jda/JDAVoiceUpdateListener.java b/src/main/java/dev/arbjerg/lavalink/libraries/jda/JDAVoiceUpdateListener.java new file mode 100644 index 0000000..56b1909 --- /dev/null +++ b/src/main/java/dev/arbjerg/lavalink/libraries/jda/JDAVoiceUpdateListener.java @@ -0,0 +1,41 @@ +package dev.arbjerg.lavalink.libraries.jda; + +import dev.arbjerg.lavalink.client.LavalinkClient; +import dev.arbjerg.lavalink.client.loadbalancing.VoiceRegion; +import dev.arbjerg.lavalink.protocol.v4.VoiceState; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; +import net.dv8tion.jda.api.hooks.VoiceDispatchInterceptor; +import org.jetbrains.annotations.NotNull; + +public class JDAVoiceUpdateListener implements VoiceDispatchInterceptor { + private final LavalinkClient lavalink; + + public JDAVoiceUpdateListener(LavalinkClient lavalink) { + this.lavalink = lavalink; + } + + @Override + public void onVoiceServerUpdate(@NotNull VoiceServerUpdate update) { + String token = update.getToken(); + String endpoint = update.getEndpoint(); + String sessionId = update.getSessionId(); + Guild guild = update.getGuild(); + + Member selfMember = guild.getMember(guild.getJDA().getSelfUser()); + if (selfMember == null) return; + + AudioChannel channel = selfMember.getVoiceState().getChannel(); + if (channel == null) return; + + VoiceState state = new VoiceState(token, endpoint, sessionId, channel.getId()); + VoiceRegion region = VoiceRegion.Companion.fromEndpoint(endpoint); + lavalink.getOrCreateLink(guild.getIdLong(), region).onVoiceServerUpdate(state); + } + + @Override + public boolean onVoiceStateUpdate(@NotNull VoiceStateUpdate update) { + return false; + } +}