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 dev.lavalink.youtube.YoutubeAudioSourceManager;
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.utils.messages.MessageCreateBuilder;
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.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.internal.utils.PermissionUtil;
/**
* Helper class for the entire music subbot instance;
* For working in tandem with Music.java
*/
public class AudioTrackHandler {
private ConcurrentHashMap<String, GuildMusicManager> musicManagers;
private AudioPlayerManager playerManager;
final HttpInterfaceManager httpInterfaceManager; //package final since TrackScheduler will also access it //not anymore, but its fine leaving it as is
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 String ytAp = null;
private List<String> votes = new ArrayList<>();
public String getUrl() {
return url;
}
public String getUserWithDiscriminator() {
return uDis;
}
public String getFormattedDuration() {
return fDur;
}
public String getYoutubeAutoplayParam() {
return ytAp;
}
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);
//ytAp is default null
}
//overload for autoplay
public TrackData(String url, long durMillis, String ytAutoplayParam) {
this.url = url;
this.uDis = "Autoplay";
this.fDur = MiscUtils.convertMillis(durMillis);
this.ytAp = ytAutoplayParam; //null for other autoplays
}
//for cloning; reset votes
public TrackData(TrackData orig) {
this.url = orig.url;
this.uDis = orig.uDis;
this.fDur = orig.fDur;
this.ytAp = orig.ytAp; //null for other autoplays
}
}
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));
//register new youtube source manager to replace the internal/broken one
playerManager.registerSourceManager(new YoutubeAudioSourceManager());
} 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.isEmpty()) {
resFuture.complete(new CommandResult(CommandResultType.NORESULT));
return;
}
if (l.size() > 100) {
resFuture.complete(new CommandResult(CommandResultType.INVALIDARGS, "Cannot queue in a playlist of more than 100 tracks."));
return;
}
if(!user.getVoiceState().inAudioChannel()) { //have to check here for pending to always work even though queueTrack would check again
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)) { //put to pending
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 everything passes try queuing
int startIndex = queueTracks(l, user) + 1;
if (n == null) { //no name == not playlist
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 {
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));
} 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") //sp is filter param, EgIQAw== is the base64 encoding of playlist option
.addParameter("search_query", search).addParameter("sp", "EgIQAw==").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(";</script>"))))
.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("lockupViewModel")) {
//return first found
return "https://www.youtube.com/playlist?list=" + renderer.getJSONObject("lockupViewModel").getString("contentId");
}
}
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;
}
String plName = playlist.getName();
List<AudioTrack> tracks = playlist.getTracks();
String plId = "";
if(playlist.isSearchResult()) {
tracks = tracks.subList(0, 1); //only get first result if search
plName = null; //no actual playlist name
} else {
if(url.contains("://soundcloud.com") || url.contains("://www.youtube.com")) //add pl id if is playlist
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(plName, tracks.stream().filter(t -> t != null).collect(Collectors.toList()));
} 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() != null && 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().inAudioChannel()) {
if (!guild.getAudioManager().isConnected()) {
VoiceChannel voice = user.getVoiceState().getChannel().asVoiceChannel();
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, String rel) 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(!rel.isEmpty()) millis = track.getPosition() + (rel.equals("-") ? -millis : millis);
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 MessageCreateBuilder 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);
MessageCreateBuilder smsg = new MessageCreateBuilder();
String fpos = MiscUtils.convertMillis(track.getPosition());
String fdur = data.getFormattedDuration();
smsg.addContent((index == 1 ? "Current" : MiscUtils.ordinal(index)) + " track: `" + track.getInfo().title + "` (" + fpos + "/" + (track.getDuration() == Long.MAX_VALUE ? "???" : fdur) + ")\n");
smsg.addContent("Author: " + track.getInfo().author + "\n");
smsg.addContent("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.addContent("URL: " + data.getUrl() + (timeTag.isEmpty() ? "" : timeTag + TimeUnit.MILLISECONDS.toSeconds(track.getPosition())));
return smsg;
}
//TODO migrate to embed?
public MessageCreateBuilder queueCheck(Guild guild, int page) throws IllegalArgumentException {
GuildMusicManager mm = getGuildMusicManager(guild);
AudioTrack playing = mm.player.getPlayingTrack();
if(playing == null) return null;
MessageCreateBuilder smsg = new MessageCreateBuilder();
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.addContent("The current queue (page " + page + "/" + maxPage + "): \n");
if (mm.scheduler.loop != null) {
smsg.addContent("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.addContent("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.addContent("[" + times + "]: `" + track.getInfo().title + "` (" + (track.getDuration() == Long.MAX_VALUE ? "N/A" : data.getFormattedDuration()) + ") requested by `" + data.getUserWithDiscriminator() + "`\n");
}
if(maxPage > page) smsg.addContent("\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();
}
//should ALWAYS be called before discarding this instance
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.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:16 AM (1 d, 10 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
b0/97/a3fd4666a051b1d0ba7a83002c1c

Event Timeline