diff --git a/pom.xml b/pom.xml
index 688081f..bfff50d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,165 +1,165 @@
4.0.0BotBot1.0-SNAPSHOTdespbotA discord bot1.81.8srcmaven-compiler-plugin3.5.11.8org.awaitilityawaitility3.0.0org.slf4jslf4j-api1.8.0-alpha2org.slf4jslf4j-log4j121.8.0-alpha2log4japache-log4j-extras1.2.17commons-loggingcommons-logging1.2com.sedmelluqlavaplayer
- 1.3.50
+ 1.3.55com.sedmelluqjda-nas1.1.0se.michaelthelin.spotifyspotify-web-api-java6.0.0commons-iocommons-io2.5com.vdurmontemoji-java3.3.0net.java.dev.jnajna4.5.0org.apache.commonscommons-lang33.6commons-iocommons-io2.5org.apache.httpcomponentshttpcore4.4.6org.jsonjson20170516net.objecthunterexp4j0.4.8net.dv8tionJDA4.1.1_155org.seleniumhq.seleniumselenium-java3.11.0org.reflectionsreflections0.9.11org.apache.commonscommons-math33.6.1org.knowm.xchartxchart3.6.4org.xerialsqlite-jdbc3.25.2com.zaxxerHikariCP3.3.1centralbintrayhttps://jcenter.bintray.com
diff --git a/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java b/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java
index 3d2e926..54df05a 100644
--- a/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java
+++ b/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java
@@ -1,463 +1,493 @@
package me.despawningbone.discordbot.command.music;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
-import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
-import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.wrapper.spotify.SpotifyApi;
import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.internal.utils.PermissionUtil;
public class AudioTrackHandler {
private ConcurrentHashMap musicManagers;
private AudioPlayerManager playerManager;
-
- final String GAPI = DiscordBot.tokens.getProperty("google"); //package final
+
+ final HttpInterfaceManager httpInterfaceManager; //package final since TrackScheduler will also access it
public ScheduledExecutorService ex = Executors.newScheduledThreadPool(2, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("audio-scheduler-%d").build());
static class TrackData {
private String url;
private String uDis;
private String fDur;
private List votes = new ArrayList<>();
public String getUrl() {
return url;
}
public String getUserWithDiscriminator() {
return uDis;
}
public String getFormattedDuration() {
return fDur;
}
public int voteSkip(User user, int req) {
if (votes.contains(user.getId()))
throw new UnsupportedOperationException("You have already voted!");
votes.add(user.getId());
if (votes.size() < req) {
return votes.size();
} else {
votes.clear();
return -1;
}
}
public TrackData(String url, User user, long durMillis) {
this.url = url;
this.uDis = user.getName() + "#" + user.getDiscriminator();
this.fDur = MiscUtils.convertMillis(durMillis);
}
//overload for autoplay
public TrackData(String url, String user, long durMillis) {
this.url = url;
this.uDis = user;
this.fDur = MiscUtils.convertMillis(durMillis);
}
}
public AudioTrackHandler() {
this.musicManagers = new ConcurrentHashMap<>();
this.playerManager = new DefaultAudioPlayerManager();
+ this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager();
playerManager.setFrameBufferDuration(1000);
playerManager.getConfiguration().setFilterHotSwapEnabled(true);
try { //register spotify source manager
playerManager.registerSourceManager(new SpotifyAudioSourceManager(new SpotifyApi.Builder().setClientId(DiscordBot.tokens.getProperty("spotifyid")).setClientSecret(DiscordBot.tokens.getProperty("spotifysecret")).build(),
playerManager, this));
} catch (Exception e) {
e.printStackTrace();
}
AudioSourceManagers.registerRemoteSources(playerManager);
AudioSourceManagers.registerLocalSource(playerManager);
}
public GuildMusicManager getGuildMusicManager(Guild guild) {
String guildId = guild.getId();
GuildMusicManager musicManager;
musicManager = musicManagers.get(guildId);
if (musicManager == null) {
musicManager = new GuildMusicManager(guild, this, playerManager);
musicManagers.put(guildId, musicManager);
}
guild.getAudioManager().setSendingHandler(musicManager.getSendHandler());
return musicManager;
}
public CompletableFuture searchAndPlay(String search, String type, Member user, TextChannel channel) throws NoSuchElementException {
CompletableFuture resFuture = new CompletableFuture<>();
String url = search;
try {
new URL(url);
} catch(MalformedURLException e) {
- url = fetchUrlFromSearch(search, type);
+ try {
+ url = fetchUrlFromSearch(search, type);
+ } catch (Exception ex) {
+ resFuture.complete(new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex)));
+ return resFuture; //something broke, halt further codes
+ }
}
if (url == null)
throw new NoSuchElementException();
load(user, url, (n, l) -> {
if (l.size() > 100) {
resFuture.complete(new CommandResult(CommandResultType.INVALIDARGS, "Cannot queue in a playlist of more than 100 tracks."));
return;
}
if(!user.getVoiceState().inVoiceChannel()) {
resFuture.complete(new CommandResult(CommandResultType.FAILURE, "You are not in a voice channel."));
return;
}
if((channel.getGuild().getAudioManager().isConnected() && !channel.getGuild().getAudioManager().getConnectedChannel().getId().equals(user.getVoiceState().getChannel().getId()))) {
resFuture.complete(new CommandResult(CommandResultType.FAILURE, "You are not in the same channel as the bot."));
return;
}
if (l.size() > 10 || l.stream().anyMatch(t -> t.getInfo().isStream)) {
GuildMusicManager mm = getGuildMusicManager(user.getGuild());
int req = (int) Math.ceil(user.getVoiceState().getChannel().getMembers().stream().filter(mem -> !mem.getUser().isBot()).count() / 2.0);
if(req > 1 && !DiscordBot.ModID.contains(user.getId())) {
if(mm.pending != null) {
resFuture.complete(new CommandResult(CommandResultType.FAILURE, "There is already a pending playlist or livestream."));
return;
}
mm.vote(user.getUser(), "tracks", req); //self vote; should never return -1 (success) coz req > 1
channel.sendMessage("Due to the total duration of your requested tracks, it has been added to pending. It will be automatically removed if it has not been approved by the users in the channel for longer than 1 minute.\n" + "Others in the channel should use `!desp music approve` to vote.").queue();
mm.pending = l;
mm.pendingCleanup = ex.schedule(() -> {
mm.clearVotes("tracks");
mm.pending = null;
mm.pendingCleanup = null;
channel.sendMessage(user.getUser().getName() + "'s" + (l.size() > 1 ? " playlist " : " livestream ") + "request has timed out.").queue();
}, 1, TimeUnit.MINUTES);
resFuture.complete(new CommandResult(CommandResultType.SUCCESS, "Pending approval"));
return;
}
}
try {
if(!l.isEmpty()) {
int startIndex = queueTracks(l, user) + 1;
if (l.size() == 1){
channel.sendMessage("Adding `" + l.get(0).getInfo().title + "` (" + (l.get(0).getDuration() == Long.MAX_VALUE ? "N/A" : l.get(0).getUserData(TrackData.class).getFormattedDuration()) + ") to the queue. [`" + startIndex + "`]").queue();
} else if (l.size() > 1) {
channel.sendMessage("Adding playlist `" + n + "` to the queue, queue now has a total of `" + (startIndex + l.size() - 1) + "` tracks.").queue();
channel.sendMessage((startIndex == 1 ? "Playing `" : "First track: `") + l.get(0).getInfo().title + "` (" + l.get(0).getUserData(TrackData.class).getFormattedDuration() + ") [`" + startIndex + "`].").queue();
}
resFuture.complete(new CommandResult(CommandResultType.SUCCESS));
} else {
resFuture.complete(new CommandResult(CommandResultType.NORESULT));
}
} catch (UnsupportedOperationException e) {
resFuture.complete(new CommandResult(CommandResultType.FAILURE, e.getMessage()));
}
}, (ex) -> resFuture.complete(ex.getStackTrace()[0].getMethodName().equals("readPlaylistName") ? new CommandResult(CommandResultType.FAILURE, "Cannot read the playlist specified. Is it private?") : new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex))));
return resFuture;
}
- private String fetchUrlFromSearch(String search, String type) {
+ private String fetchUrlFromSearch(String search, String type) throws IOException {
if (type.startsWith("youtube")) {
type = type.substring(8, type.length());
if (type.equals("video")) {
return "ytsearch:" + search;
- } else { //only use yt api when playlist (and autoplay) due to strict rate limit
- try { //NOADD check if theres a way to search for playlists and vids at the same time //using different commands now
- InputStream input = new URL("https://www.googleapis.com/youtube/v3/search?part=snippet%20&q=" + URLEncoder.encode(search, "UTF-8") + "%20&type=" + type + "%20&key=" + GAPI).openStream();
- JSONObject result = new JSONObject(new JSONTokener(new InputStreamReader(input, "UTF-8"))).getJSONArray("items").getJSONObject(0);
- JSONObject id = result.getJSONObject("id");
- String vidID = id.getString(type + "Id"); //can switch to pure playlist impl tbh i aint even using this api for normal vids
- return "https://www.youtube.com/" + (type.equals("video") ? "watch?v=" : "playlist?list=") + vidID;
- } catch (IOException e) {
- e.printStackTrace();
- return null;
- } catch (JSONException e) {
- return null;
+ } else { //scrape with our own code since YoutubeSearchProvider only scrapes videos
+ //NOADD check if theres a way to search for playlists and vids at the same time //using different commands now
+ try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) {
+ URI url = new URIBuilder("https://www.youtube.com/results").addParameter("search_query", search).build();
+
+ try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(url))) { //connection code borrowed from lavaplayer's YoutubeSearchProvider
+ int statusCode = response.getStatusLine().getStatusCode();
+ if (!HttpClientTools.isSuccessWithContent(statusCode)) {
+ throw new IOException("Invalid status code for search response: " + statusCode);
+ }
+
+ //start parsing
+ String data = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
+ data = data.substring(data.indexOf("var ytInitialData = ") + 20);
+ JSONObject result = new JSONObject(new JSONTokener(data.substring(0, data.indexOf(";\n"))))
+ .getJSONObject("contents").getJSONObject("twoColumnSearchResultsRenderer").getJSONObject("primaryContents")
+ .getJSONObject("sectionListRenderer").getJSONArray("contents").getJSONObject(0).getJSONObject("itemSectionRenderer");
+
+ //iterate to get first playlist
+ for(Object obj : result.getJSONArray("contents")) {
+ JSONObject renderer = (JSONObject) obj;
+ if(renderer.has("playlistRenderer")) {
+ return "https://www.youtube.com" + renderer.getJSONObject("playlistRenderer").getJSONObject("navigationEndpoint")
+ .getJSONObject("commandMetadata").getJSONObject("webCommandMetadata").getString("url"); //return first found
+ }
+ }
+
+ return null; //if no result
+ }
+ } catch (URISyntaxException e) {
+ e.printStackTrace(); //should be unreachable
}
}
} else if (type.equals("soundcloud")) {
return "scsearch:" + search;
}
throw new UnsupportedOperationException("This provider is not implemented yet!");
}
//package private
void load(Member user, String url, BiConsumer> resultHandler, Consumer exceptionally) {
playerManager.loadItemOrdered(getGuildMusicManager(user.getGuild()), url, new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack track) {
try {
track.setUserData(new TrackData(track.getInfo().uri, user.getUser(), track.getDuration()));
resultHandler.accept(null, Arrays.asList(track));
} catch(Exception e) {
exceptionally.accept(e); //so i dont lose my sanity over silenced errors
}
}
@Override
public void playlistLoaded(AudioPlaylist playlist) {
try {
if(playlist.getTracks().size() == 0) { //somehow its possible; do the same as noResult()
if(url.startsWith("ytsearch:")) load(user, url.replaceFirst("yt", "sc"), resultHandler, exceptionally); //searches in soundcloud, since apparently yt pretty frequently returns no result
else resultHandler.accept(null, new ArrayList<>());
return;
}
List tracks = playlist.isSearchResult() ? playlist.getTracks().subList(0, 1) : playlist.getTracks();
String plId = "";
if (!playlist.isSearchResult())
if(url.contains("://soundcloud.com") || url.contains("://www.youtube.com"))
plId = url.contains("://soundcloud.com") ? "?in=" + url.split("soundcloud.com/")[1] : "&list=" + url.split("list=")[1].split("&")[0];
for (AudioTrack track : tracks) //TODO tell users that we skipped some tracks?
if(track != null) track.setUserData(new TrackData(track.getInfo().uri + plId, user.getUser(), track.getDuration()));
if (playlist.getSelectedTrack() != null)
tracks.add(0, tracks.remove(tracks.indexOf(playlist.getSelectedTrack()))); //shift selected track to first track
resultHandler.accept(playlist.getName(), tracks.stream().filter(t -> t != null).collect(Collectors.toList())); //only get first result if search
} catch(Exception e) {
exceptionally.accept(e); //so i dont lose my sanity over silenced errors
}
}
@Override
public void noMatches() {
if(url.startsWith("ytsearch:")) load(user, url.replaceFirst("yt", "sc"), resultHandler, exceptionally); //searches in soundcloud, since apparently yt pretty frequently returns no result
else resultHandler.accept(null, new ArrayList<>());
}
@Override
public void loadFailed(FriendlyException exception) {
if(exception.getMessage().contains("Unknown file format.") && url.contains("open.spotify.com")) {
resultHandler.accept(null, new ArrayList<>()); //TODO TEMPORARY FIX
} else {
exceptionally.accept(exception.getCause() != null ? exception.getCause() : exception);
}
}
});
}
public int queueTracks(List tracks, Member user) throws UnsupportedOperationException {
Guild guild = user.getGuild();
GuildMusicManager musicManager = getGuildMusicManager(guild);
int startIndex = musicManager.scheduler.getQueueSize() + (musicManager.player.getPlayingTrack() != null ? 1 : 0);
if (user.getVoiceState().inVoiceChannel()) {
if (!guild.getAudioManager().isConnected() && !guild.getAudioManager().isAttemptingToConnect()) {
VoiceChannel voice = user.getVoiceState().getChannel();
if (PermissionUtil.checkPermission(voice, guild.getSelfMember(), Permission.VOICE_CONNECT, Permission.VOICE_SPEAK)) {
guild.getAudioManager().openAudioConnection(voice); //already checked permissions so no need to try catch
} else {
throw new UnsupportedOperationException("The bot cannot play music in that channel.");
}
}
} else {
throw new UnsupportedOperationException("You are currently not in a voice channel.");
}
try {
if (!guild.getAudioManager().isConnected()) {
Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> guild.getAudioManager().isConnected());
}
} catch (ConditionTimeoutException e) {
throw new UnsupportedOperationException("Error while connecting to voice channel: The connection timed out.");
}
if (guild.getAudioManager().getConnectedChannel().equals(user.getVoiceState().getChannel())) {
for (AudioTrack track : tracks)
musicManager.scheduler.queue(track); //nulls should already be handled; if it aint its my fault lmao
} else {
throw new UnsupportedOperationException("You are currently not in the same channel as the bot.");
}
return startIndex; //if it successfully returned it means that nothing failed
}
public String skipTrack(Guild guild) {
GuildMusicManager musicManager = getGuildMusicManager(guild);
musicManager.scheduler.nextTrack();
musicManager.player.setPaused(false); //implicit resume
try {
return musicManager.player.getPlayingTrack().getInfo().title;
} catch (NullPointerException e) {
musicManager.scheduler.loop = null;
return null;
}
}
public String setTrackPosition(Guild guild, long hour, long min, long sec) throws IllegalArgumentException {
GuildMusicManager mm = getGuildMusicManager(guild);
Long millis = TimeUnit.HOURS.toMillis(hour) + TimeUnit.MINUTES.toMillis(min) + TimeUnit.SECONDS.toMillis(sec);
AudioTrack track = mm.player.getPlayingTrack();
if (track.getDuration() > millis && !track.getInfo().isStream) {
track.setPosition(millis);
return MiscUtils.convertMillis(track.getPosition());
} else if (track.getInfo().isStream) {
throw new IllegalArgumentException("You cannot set the track time in a stream!");
} else {
throw new IllegalArgumentException("You cannot set the track time over the track duration.");
}
}
//not zero-based
public MessageBuilder getTrackInfo(Guild guild, int index) { //TODO add views, etc by storing them when getting with lavaplayer?
GuildMusicManager mm = getGuildMusicManager(guild);
if(index - 1 > mm.scheduler.getQueueSize() || index < 1) return null;
AudioTrack track = index == 1 ? mm.player.getPlayingTrack() : mm.scheduler.findTracks(index - 1, 1).get(0);
TrackData data = track.getUserData(TrackData.class);
MessageBuilder smsg = new MessageBuilder();
String fpos = MiscUtils.convertMillis(track.getPosition());
String fdur = data.getFormattedDuration();
smsg.append((index == 1 ? "Current" : MiscUtils.ordinal(index)) + " track: `" + track.getInfo().title + "` (" + fpos + "/" + (track.getDuration() == Long.MAX_VALUE ? "???" : fdur) + ")\n");
smsg.append("Author: " + track.getInfo().author + "\n");
smsg.append("Requested by: `" + data.getUserWithDiscriminator() + "`\n");
String timeTag = "";
if(data.getUrl().startsWith("https://www.youtube.com")) timeTag = "&=";
else if(data.getUrl().startsWith("https://soundcloud.com")) timeTag = "#=";
smsg.append("URL: " + data.getUrl() + (timeTag.isEmpty() ? "" : timeTag + TimeUnit.MILLISECONDS.toSeconds(track.getPosition())));
return smsg;
}
//TODO migrate to embed?
public MessageBuilder queueCheck(Guild guild, int page) throws IllegalArgumentException {
GuildMusicManager mm = getGuildMusicManager(guild);
AudioTrack playing = mm.player.getPlayingTrack();
if(playing == null) return null;
MessageBuilder smsg = new MessageBuilder();
List tracks = mm.scheduler.findTracks(1, Integer.MAX_VALUE).stream().filter(a -> a != null).collect(Collectors.toList()); //get all tracks in queue
tracks.add(0, playing);
int maxPage = (int) Math.ceil(tracks.size() / 10f);
if(page > maxPage) throw new IllegalArgumentException("There is no such page.");
smsg.append("The current queue (page " + page + "/" + maxPage + "): \n");
if (mm.scheduler.loop != null) {
smsg.append("There is a total of `" + tracks.size() + "` tracks " + (mm.scheduler.loop.equals("loop") ? "looping" : "in autoplay") + ".\n\n");
} else {
long millis = 0;
for(AudioTrack track : tracks)
millis += track.getDuration();
smsg.append("There is a total of `" + tracks.size() + "` tracks queued" + ((millis == Long.MAX_VALUE || millis < 0) ? ".\n\n" : ", with a total duration of `" + MiscUtils.convertMillis(millis - playing.getPosition()) + "`.\n\n"));
}
int times = (page - 1) * 10;
for (AudioTrack track : tracks.subList((page - 1) * 10, Math.min(tracks.size(), page * 10))) {
times++;
TrackData data = track.getUserData(TrackData.class);
smsg.append("[" + times + "]: `" + track.getInfo().title + "` (" + (track.getDuration() == Long.MAX_VALUE ? "N/A" : data.getFormattedDuration()) + ") requested by `" + data.getUserWithDiscriminator() + "`\n");
}
if(maxPage > page) smsg.append("\nDo `!desp music queue " + (page + 1) + "` to see the next page.");
return smsg;
}
public boolean togglePause(Guild guild, boolean pause) throws IllegalStateException {
GuildMusicManager mm = getGuildMusicManager(guild);
if (mm.player.isPaused() == !pause) {
mm.player.setPaused(pause);
return mm.player.isPaused();
} else {
throw new IllegalStateException("The player is already " + (mm.player.isPaused() ? "paused!" : "unpaused!"));
}
}
public boolean toggleLoopQueue(Guild guild, String type) {
type = type.toLowerCase();
GuildMusicManager mm = getGuildMusicManager(guild);
if (mm.scheduler.loop == null || !mm.scheduler.loop.equals(type)) {
mm.scheduler.loop = type;
if (type != null && type.equals("autoplay") && mm.scheduler.getQueueSize() < 1) {
mm.scheduler.queueAutoplay(mm.player.getPlayingTrack());
}
return true;
} else { //remove autoplay queued track when disabling autoplay?
if (mm.scheduler.loop.equals("autoplay") && mm.scheduler.getQueueSize() == 1 && mm.scheduler.findTracks(1, 1).get(0).getUserData(TrackData.class).uDis.equals("Autoplay")) { //including autoplay, theres only 2 tracks; only remove tracks that is autoplayed
mm.scheduler.removeTrack(1);
}
mm.scheduler.loop = null;
return false;
}
}
public void stopAndClearQueue(Guild guild) {
GuildMusicManager mm = getGuildMusicManager(guild);
mm.pending = null;
mm.pendingCleanup = null;
mm.clearQueueCleanup = null;
mm.scheduler.loop = null;
mm.scheduler.clearSchedulerQueue();
mm.clearAllVotes();
mm.player.stopTrack();
mm.player.setPaused(false);
guild.getAudioManager().closeAudioConnection();
}
public void shutdown() {
musicManagers.forEach((s, mm) -> {
//System.out.println(DiscordBot.mainJDA.getGuildById(s).getName());
mm.player.destroy();
mm.scheduler.clearSchedulerQueue();
AudioManager man = DiscordBot.mainJDA.getGuildById(s).getAudioManager();
if(man.isConnected() || man.isAttemptingToConnect()) {
man.closeAudioConnection();
DiscordBot.lastMusicCmd.get(s).sendMessage("The music bot is going into maintenance and it will now disconnect. Sorry for the inconvenience.").queue();
}
});
musicManagers = null; //so further operations wont be possible even if i forgot to set this instance to null
playerManager.shutdown();
ex.shutdown();
}
}
diff --git a/src/me/despawningbone/discordbot/command/music/TrackScheduler.java b/src/me/despawningbone/discordbot/command/music/TrackScheduler.java
index d29f201..defb8cf 100644
--- a/src/me/despawningbone/discordbot/command/music/TrackScheduler.java
+++ b/src/me/despawningbone/discordbot/command/music/TrackScheduler.java
@@ -1,200 +1,240 @@
package me.despawningbone.discordbot.command.music;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.music.AudioTrackHandler.TrackData;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.URL;
+import java.io.IOException;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
* This class schedules tracks for the audio player. It contains the queue of
* tracks.
*/
public class TrackScheduler extends AudioEventAdapter {
private final AudioPlayer player;
private final BlockingQueue queue;
private AudioTrackHandler ap;
private Guild guild;
public String loop = null; //way too lazy to use getter setters lmao
/**
* @param player
* The audio player this scheduler uses
*/
public TrackScheduler(Guild parent, AudioTrackHandler handler, AudioPlayer player) {
this.player = player;
this.queue = new LinkedBlockingQueue<>();
this.ap = handler;
this.guild = parent;
}
/**
* Add the next track to queue or play right away if nothing is in the
* queue.
*
* @param track
* The track to play or add to queue.
*/
public void queue(AudioTrack track) {
// Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If
// something is playing, it returns false and does nothing. In that case the player was already playing so this
// track goes to the queue instead.
if (!player.startTrack(track, true)) {
queue.offer(track);
} else if (loop != null && loop.equals("autoplay")) { //i dont think this is needed as people need to play something before autoplay can be toggled anyways
queueAutoplay(track);
}
}
/**
* Start the next track, stopping the current one if it is playing.
*/
public void nextTrack() { //DONE rewrite to not include q.remove here so that stuff like interrupted wont break the queue?
// Start the next track, regardless of if something is already playing or not. In case queue was empty, we are
// giving null to startTrack, which is a valid argument and will simply stop the player.
AudioTrack track = queue.poll();
player.startTrack(track, false);
if(track == null) {
//System.out.println("finished"); //debug
loop = null;
delayCloseConnection(player); //required because if not it will throw InterruptedException
}
} //seems to be called internally somehow; even when mayStartNext is false (REPLACED, STOPPED etc) this still fires
//NVM ITS CALLED FROM AudioTrackHandler.skipTrack() LOL
public void queueAutoplay(AudioTrack track) { //check duplicate please, some can get into a dead loop like cosMo@暴走P - WalpurgisNacht and Ice - 絶 //well randoming it works
ap.ex.submit(() -> { //async so it can free up the event
- try {
+ try (HttpInterface httpInterface = ap.httpInterfaceManager.getInterface()) {
//System.out.println("autoplay");
- InputStream input = new URL("https://www.googleapis.com/youtube/v3/search?part=snippet&relatedToVideoId=" + track.getIdentifier() + "&type=video&key=" + ap.GAPI).openStream();
- JSONTokener result = new JSONTokener(new InputStreamReader(input, "UTF-8"));
- Member temp;
- try {
- temp = guild.getMemberByTag(track.getUserData(TrackData.class).getUserWithDiscriminator());
- } catch (IllegalArgumentException e) { //track was autoplay queued before this
- temp = guild.getMemberById(DiscordBot.BotID); //fallback
+
+ URI orig = new URIBuilder("https://www.youtube.com/watch").addParameter("v", track.getIdentifier()).build();
+
+ try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(orig))) { //connection code borrowed from lavaplayer's YoutubeSearchProvider
+ int statusCode = response.getStatusLine().getStatusCode();
+ if (!HttpClientTools.isSuccessWithContent(statusCode)) {
+ throw new IOException("Invalid status code for search response: " + statusCode);
+ }
+
+ //start parsing
+ String data = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
+ data = data.substring(data.indexOf("window[\"ytInitialData\"] = ") + 26);
+
+ for (int i = 0; i < 5 && !data.startsWith("{"); i++) { //reconnect in case it doesnt fetch correctly; give up after 5 tries (should be more than enough)
+ response.close();
+ try(CloseableHttpResponse retry = httpInterface.execute(new HttpGet(orig))) {
+ data = IOUtils.toString(retry.getEntity().getContent(), "UTF-8");
+ data = data.substring(data.indexOf("window[\"ytInitialData\"] = ") + 26);
+ }
+ }
+
+ JSONArray arr = new JSONObject(new JSONTokener(data.substring(0, data.indexOf(";\n"))))
+ .getJSONObject("contents").getJSONObject("twoColumnWatchNextResults").getJSONObject("secondaryResults")
+ .getJSONObject("secondaryResults").getJSONArray("results");
+
+ //DONE reimplement random selection between top 2 results to alleviate infinite loops
+ JSONObject result = ThreadLocalRandom.current().nextBoolean() ?
+ arr.getJSONObject(0).getJSONObject("compactAutoplayRenderer").getJSONArray("contents").getJSONObject(0).getJSONObject("compactVideoRenderer") //get autoplay video renderer
+ : null;
+ if(result == null)
+ for(Object obj : arr) { //else find first video renderer
+ JSONObject renderer = (JSONObject) obj;
+ if(renderer.has("compactVideoRenderer")) {
+ result = renderer.getJSONObject("compactVideoRenderer");
+ }
+ }
+
+ //JSONObject result = new JSONObject(new JSONTokener(data.substring(0, data.indexOf(";\n")))).getJSONObject("webWatchNextResponseExtensionData")
+ // .getJSONObject("contents").getJSONObject("twoColumnWatchNextResults").getJSONObject("secondaryResults").getJSONObject("secondaryResults")
+ // .getJSONObject("results").getJSONObject("compactAutoplayRenderer").getJSONArray("contents").getJSONObject(0).getJSONObject("compactVideoRenderer");
+ String url = "https://youtube.com/watch?v=" + result.getString("videoId");
+
+ //load
+ Member user = guild.getMemberById(DiscordBot.BotID); //no need to use original track user as it would introduce performance overhead and other complications when leaving channels
+ ap.load(user, url, (n, l) -> {
+ AudioTrack auto = l.get(0);
+ auto.setUserData(new TrackData(auto.getInfo().uri, "Autoplay", auto.getDuration())); //change name to autoplay
+ ap.queueTracks(l, user); //no need sublist coz l is always a single video
+ }, ex -> {
+ ex.printStackTrace();
+ DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track: " + ex.getMessage()).queue();
+ loop = null;
+ });
}
- final Member user = temp;
- int t = ThreadLocalRandom.current().nextInt(2);
- String url = "https://youtube.com/watch?v=" + new JSONObject(result).getJSONArray("items").getJSONObject(t).getJSONObject("id").getString("videoId");
- ap.load(user, url, (n, l) -> {
- AudioTrack auto = l.get(0);
- auto.setUserData(new TrackData(auto.getInfo().uri, "Autoplay", auto.getDuration()));
- ap.queueTracks(l.subList(0, 1), user);
- }, ex -> {
- ex.printStackTrace();
- DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track: " + ex.getMessage()).queue();
- loop = null;
- });
} catch (Exception e) {
e.printStackTrace();
DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track. Is the last track a youtube video?").queue();
loop = null;
}
});
}
public List findTracks(int startIndex, int range) {
AudioTrack[] a = queue.toArray(new AudioTrack[queue.size()]);
int i = startIndex - 1 + range < 0 ? Integer.MAX_VALUE : startIndex - 1 + range; //prevent overflow
AudioTrack[] t = Arrays.copyOfRange(a, startIndex - 1, Math.min(queue.size() + 1, i)); //accounts for first track player thats not in queue
return Arrays.asList(t);
}
public AudioTrack removeTrack(int num) {
Iterator i = queue.iterator();
num = num - 1;
for (int times = 0; i.hasNext(); times++) {
AudioTrack removed = i.next();
if (num == times) {
i.remove();
return removed;
}
}
return null;
}
public AudioTrack moveTrack(int from, int to) {
List q = new ArrayList<>(Arrays.asList(queue.toArray(new AudioTrack[queue.size()])));
AudioTrack track = q.remove(from);
q.add(to, track);
synchronized (queue) { //obtain lock and operate before releasing or else queue might not be complete
queue.clear();
for(AudioTrack t : q) queue.offer(t);
}
return track;
}
public void shuffleQueue() {
List q = Arrays.asList(queue.toArray(new AudioTrack[queue.size()]));
Collections.shuffle(q);
synchronized (queue) { //obtain lock and operate before releasing or else queue might not be complete
queue.clear();
for(AudioTrack t : q) queue.offer(t);
}
}
public void clearSchedulerQueue() {
queue.clear();
}
public int getQueueSize() {
return queue.size();
}
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
// Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED or REPLACED)
//System.out.println(endReason); //debug
boolean mayStartNext = endReason.mayStartNext;
if (mayStartNext) {
//doesnt queue clone if skipped
if (loop != null && loop.equals("loop")) { //so what the hecc if loop is null and i do loop.equals("Loop") it freezes the thread
AudioTrack clone = track.makeClone();
TrackData origData = clone.getUserData(TrackData.class);
clone.setUserData(new TrackData(origData.getUrl(), origData.getUserWithDiscriminator(), clone.getDuration())); //wipe votes
queue.offer(clone);
}
nextTrack();
}
if(mayStartNext || endReason == AudioTrackEndReason.REPLACED) { //queues new if skipped too
if (loop != null && loop.equals("autoplay") && queue.size() < 1) {
queueAutoplay(player.getPlayingTrack());
}
}
}
private void delayCloseConnection(AudioPlayer player) { //TODO configurable delay; dont leave if new things play within the delay
ap.ex.schedule(() -> guild.getAudioManager().closeAudioConnection(), 1, TimeUnit.MILLISECONDS);
}
}
\ No newline at end of file