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) {
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();
}, (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))));
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
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) {
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("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"));
} 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
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
+ (handler.toggleLoopQueue(c.getGuild(), "autoplay") ? "on.\nIt will end once someone joins the music channel." : "off."))
.queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You are currently not alone in the music channel, therefore this feature is disabled.");
}
}, "", null, "Auto-queues related tracks when you are alone!",
Arrays.asList(" * Currently only works if the last track in the queue is a youtube video.", " Note: it will autoplay indefinitely until you toggle it again or someone joins."));
registerSubCommand("loop", Arrays.asList("l", "loopqueue"), (c, u, m, a) -> {
if(vc == null) vc = requester.getVoiceState().getChannel().asVoiceChannel(); //fallback to requester, usually due to bot not joined yet (first track needs approval)
if(vc == null) vc = c.getGuild().getMember(u).getVoiceState().getChannel().asVoiceChannel(); //fallback to approver in case requester left voice channel
try {
int req = (int) Math.ceil(vc.getMembers().stream().filter(mem -> !mem.getUser().isBot()).count() / 2.0);
List<AudioTrack> tracks = mm.pending; //have to place before vote or else it gets wiped
int votes = mm.vote(u, "tracks", req);
if (votes == -1) { //automatically cleaned up in guildmusicmanager already
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter a valid timestamp with colons.");
} catch (IllegalArgumentException e) {
return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
}
}, "[+/-][hr:]<min:sec>", Arrays.asList("4:52:21", "2:00", "+30:00", "-1:23:45"), "Set the playing position in the current track!",
Arrays.asList("timestamps without signs are absolute, whereas `+`/`-` means go forward or backward from the current position for the amount of time specified respectively.", " * the person who requested the current track can always set the position, regardless of perms."),
registerSubCommand("removetrack", Arrays.asList("rt", "r", "skiptrack", "st"), (c, u, m, a) -> {
if(a.length > 0) {
try {
int num = Integer.parseInt(a[0]);
if(num == 1) return new CommandResult(CommandResultType.INVALIDARGS, "You cannot remove the current track from the queue with this command. Use !desp music skip instead.");
if(num < 1) return new CommandResult(CommandResultType.INVALIDARGS, "The index you entered is invalid.");
return new CommandResult(CommandResultType.INVALIDARGS, "Please valid indices.");
}
}, "<fromindex> <toindex>", Arrays.asList("2 3"), "Move a track to the specified index.", Arrays.asList(" * you can also run this command regardless of perms if you are alone with the bot.")
c.sendMessage("Successfully shuffled the queue.").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You are currently not alone in the music channel, therefore this feature is disabled.");
}
}, "", null, "Shuffle the tracks in the queue when you are alone!", null);
//TODO make vote system for this
registerSubCommand("clear", Arrays.asList("disconnect", "dc", "stop", "clearqueue"), (c, u, m, a) -> {
handler.stopAndClearQueue(c.getGuild());
c.sendMessage("The queue has been cleared.").queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "", null, "Clear the queue and stop the player.", Arrays.asList(" * you can also run this command regardless of perms if you are alone with the bot.")
return new CommandResult(CommandResultType.FAILURE, e.getMessage());
} catch(NullPointerException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "There is nothing playing currently! Please specify a song title to search the lyrics up.");
} catch(IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
}, "[search words]", Arrays.asList(""), "Search some lyrics up!", Arrays.asList(" * if you did not specify any search words, the bot will try to fetch the lyrics of the current track playing, if any."));
//TODO equalizer persistence?
//make this premium?
registerSubCommand("equalizer", Arrays.asList("eq"), (c, u, m, a) -> {
GuildMusicManager mm = handler.getGuildMusicManager(c.getGuild());
Float[] vals;
if(a.length == 0) {
vals = mm.getCurrentGain();
} else {
try {
try {
if(a.length != 15) throw new NumberFormatException();
vals = new Float[15];
for(int i = 0; i < a.length; i++) {
vals[i] = Float.parseFloat(a[i]);
}
mm.setGain(0, vals);
} catch(NumberFormatException e) {
vals = mm.setPresetGain(a[0]);
}
} catch(IllegalArgumentException e2) {
return new CommandResult(CommandResultType.INVALIDARGS, e2.getMessage());
}
}
//formatting
DecimalFormat df = new DecimalFormat("0.00");
df.setPositivePrefix("+");
EmbedBuilder eb = new EmbedBuilder();
eb.setTitle("Current equalizer graph");
eb.appendDescription("```\n");
for(double line = 0.25; line >= -0.25; line -= 0.05) {
return new CommandResult(CommandResultType.SUCCESS);
}, "[bandvalues/presetname]", Arrays.asList("", "0.09 0.07 0.07 0.01 0 0 -0.02 -0.02 0.03 0.03 0.05 0.07 0.09 0.1 0.1", "bassboost"), "Sets the equalizer for the music player in this guild!",
Arrays.asList("Accepts 15 bands with values ranging from -0.25 to 0.25, where -0.25 is muted and 0.25 is double volume.", "* presets include: `bassboost`, `default`, `rock`.", "Input nothing to return the current settings.", "",
"Note: you might experience some audio cracking for band values >0.1, since amplifying volumes remotely does not work well.", "It is recommended to use values from -0.25 to 0.1, and turning discord volume up instead.")
if(Arrays.asList("move", "clear").contains(sub.getName())) { //should always be connected to vc
if(channel.getGuild().getAudioManager().getConnectedChannel().getMembers().size() == 2) { //alone; no need to check if the other member is requester, since its checked before
CommandResult res = super.execute(channel, author, msg, args); //pass to subcommand handler for perms checking
if(res.getResultType() == CommandResultType.NOPERMS && Arrays.asList("forceskip", "skip", "setposition", "removetrack").contains(sub.getName())) { //add back info to command result
res = new CommandResult(CommandResultType.INVALIDARGS, res.getMessage().getContent().split(" to execute")[0] + ", or have requested the track to execute this command.");
}
return res;
}
//maybe move the lyrics stuff elsewhere?
private final String geniusAuth = DiscordBot.tokens.getProperty("genius");
//annotated usually isnt actual lyrics, but the following are known to be some
//private ExecutorService executor = Executors.newCachedThreadPool(); //unfortunately i need to finish the api search to get the url to start the html scrape, which means i cannot use future for multithread here
public List<MessageEmbed> getLyrics(String search) throws IOException { //DONE dont splice words between embeds; make whole sentence to be spliced instead
//appends metadata portion to the given embed (should be the last one)
private void setFinalLyricsEmbed(EmbedBuilder eb, JSONObject main, Element html) { //change the format for the supplementary info in artists and albums from italic to sth else?
try {
String name = main.getJSONObject("primary_artist").getString("name");
- JSONObject preload = new JSONObject(StringEscapeUtils.unescapeJson(temp));
- JSONObject song = preload.getJSONObject("entities").getJSONObject("songs").getJSONObject(Integer.toString(preload.getJSONObject("songPage").getInt("song")));
+ JSONObject preload = new JSONObject(StringEscapeUtils.unescapeJson(temp));
+ JSONObject song = preload.getJSONObject("entities").getJSONObject("songs").getJSONObject(Integer.toString(preload.getJSONObject("songPage").getInt("song")));
* Boilerplate from https://github.com/sedmelluq/lavaplayer/tree/master/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda
*/
public class TrackScheduler extends AudioEventAdapter {
private final AudioPlayer player;
private final BlockingQueue<AudioTrack> queue;
private AudioTrackHandler ap;
private Guild guild;
public String loop = null; //way too lazy to use getter setters lmao
public TrackScheduler(Guild parent, AudioTrackHandler handler, AudioPlayer player) {
this.player = player;
this.queue = new LinkedBlockingQueue<>();
this.ap = handler;
this.guild = parent;
}
public void queue(AudioTrack track) {
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);
}
}
public void nextTrack() { //DONE rewrite to not include q.remove here so that stuff like interrupted wont break the queue?
AudioTrack track = queue.poll();
player.startTrack(track, false);
if(track == null) {
//System.out.println("finished"); //debug
loop = null; //reset loop and autoplay tracking
delayCloseConnection(player); //required because if not it will throw InterruptedException
}
} //externally called from AudioTrackHandler.skipTrack()
public void queueAutoplay(AudioTrack track) { //DONE check duplicate please, some can get into a dead loop like cosMo@暴走P - WalpurgisNacht and Ice - 絶 //using yt tracking params now; still can get into loops but random should fix
ap.ex.submit(() -> { //async so it can free up the event
try {
//System.out.println("autoplay"); //debug
- Connection con = Jsoup.connect("https://www.youtube.com/watch?v=" + track.getIdentifier() + "&pbj=1");
-
+ Connection con = Jsoup.connect("https://www.youtube.com/watch?v=" + track.getIdentifier());
+
+ //TODO change to using proper tracking params (maybe VISITOR_INFO1_LIVE or sth?)
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, auto.getDuration(), autoplay.toString())); //set new tracking
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;
});
} 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<AudioTrack> 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<AudioTrack> 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<AudioTrack> 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