fix: music system improvements, radio refactoring, lavalink config

This commit is contained in:
loki5512344 2026-07-03 01:24:36 +02:00
parent a764ab9b2d
commit 108f63f7ec
8 changed files with 137 additions and 136 deletions

View file

@ -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}

View file

@ -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

View file

@ -53,7 +53,7 @@ public class LavalinkManager {
.setName("main")
.setServerUri(lavalinkConfig.getWsUri())
.setPassword(lavalinkConfig.getPassword())
.setHttpTimeout(10000L)
.setHttpTimeout(30000L)
.build());
registerListeners();

View file

@ -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) {

View file

@ -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(

View file

@ -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(

View file

@ -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<Long, GuildRadioState> radioStates;
private final Map<Long, GuildRadioState> 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;
}
}

View file

@ -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;
}
}