Page MenuHomedesp's stash

AudioTrackHandler.java
No OneTemporary

AudioTrackHandler.java

package me.despawningbone.discordbot.command.music;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
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.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<String, GuildMusicManager> musicManagers;
private AudioPlayerManager playerManager;
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<String> 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<CommandResult> searchAndPlay(String search, String type, Member user, TextChannel channel) throws NoSuchElementException {
CompletableFuture<CommandResult> resFuture = new CompletableFuture<>();
String url = search;
try {
new URL(url);
} catch(MalformedURLException e) {
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) throws IOException {
if (type.startsWith("youtube")) {
type = type.substring(8, type.length());
if (type.equals("video")) {
return "ytsearch:" + search;
} 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<String, List<AudioTrack>> resultHandler, Consumer<Throwable> 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<AudioTrack> 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<AudioTrack> 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<AudioTrack> 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();
}
}

File Metadata

Mime Type
text/x-java
Expires
Mon, Jul 7, 5:35 PM (1 d, 4 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ee/81/aa578a457acd4ac87f2e4d31a594

Event Timeline