/* * Skybot, a multipurpose discord bot * Copyright (C) 2017 - 2020 Duncan "duncte123" Sterken & Ramid "ramidzkh" Khan & Maurice R S "Sanduhr32" * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ml.duncte123.skybot.audio; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import lavalink.client.player.IPlayer; import lavalink.client.player.event.AudioEventAdapterWrapped; import ml.duncte123.skybot.Author; import ml.duncte123.skybot.exceptions.LimitReachedException; import ml.duncte123.skybot.extensions.AudioTrackKt; import ml.duncte123.skybot.objects.TrackUserData; import ml.duncte123.skybot.utils.Debouncer; import net.dv8tion.jda.api.EmbedBuilder; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.TimeUnit; import static me.duncte123.botcommons.messaging.MessageUtils.sendEmbed; import static me.duncte123.botcommons.messaging.MessageUtils.sendMsg; import static ml.duncte123.skybot.SkyBot.getInstance; @Author(nickname = "duncte123", author = "Duncan Sterken") public class TrackScheduler extends AudioEventAdapterWrapped { public static final int QUEUE_SIZE = 50; public final Queue<AudioTrack> queue; private static long DEBOUNCE_INTERVAL = TimeUnit.SECONDS.toMillis(5); private static final Logger logger = LoggerFactory.getLogger(TrackScheduler.class); private final IPlayer player; private final GuildMusicManager guildMusicManager; private final Debouncer<String> messageDebouncer; private boolean repeating = false; private boolean repeatPlayList = false; TrackScheduler(IPlayer player, GuildMusicManager guildMusicManager) { this.player = player; this.queue = new LinkedList<>(); this.guildMusicManager = guildMusicManager; this.messageDebouncer = new Debouncer<>((msg) -> sendMsg(guildMusicManager.getLatestChannel(), msg, null, (t) -> {}) , DEBOUNCE_INTERVAL); } public void queue(AudioTrack track, boolean isPatron) throws LimitReachedException { if (queue.size() >= QUEUE_SIZE && !isPatron) { throw new LimitReachedException("The queue is full", QUEUE_SIZE); } if (player.getPlayingTrack() != null) { queue.offer(track); } else { player.playTrack(track); } } /** * This is a special case for the skip command where it has to announce the next track * due to it being a user interaction */ public void specialSkipCase() { // Get the currently playing track final AudioTrack playingTrack = this.player.getPlayingTrack(); // Set in the data that it was from a skip playingTrack.getUserData(TrackUserData.class).setWasFromSkip(true); // Seek to the end to start the next track // We seek to use the normal flow that allows for repeating as well this.player.seekTo(playingTrack.getDuration()); } private void skipTrack() { skipTracks(1); } public void skipTracks(int count) { AudioTrack nextTrack = null; for (int i = 0; i < count; i++) { nextTrack = queue.poll(); } if (nextTrack == null) { player.stopTrack(); sendMsg(guildMusicManager.getLatestChannel(), "Queue concluded"); } else { player.playTrack(nextTrack); } } @Override public void onTrackStart(AudioPlayer player, AudioTrack track) { final TrackUserData data = track.getUserData(TrackUserData.class); // If the track was a skipped track or we announce tracks if (data.getWasFromSkip() || this.guildMusicManager.isAnnounceTracks()) { // Reset the was from skip status data.setWasFromSkip(false); final EmbedBuilder message = AudioTrackKt.toEmbed( track, this.guildMusicManager, getInstance().getShardManager(), false ); sendEmbed(this.guildMusicManager.getLatestChannel(), message); } } @Override public void onTrackEnd(AudioPlayer player, AudioTrack lastTrack, AudioTrackEndReason endReason) { logger.debug("track ended"); if (!endReason.mayStartNext) { return; } // Get if the track was from a skip event final boolean wasFromSkip = lastTrack.getUserData(TrackUserData.class).getWasFromSkip(); logger.debug("can start"); if (!repeating) { logger.debug("starting next track"); skipTrack(); return; } logger.debug("repeating"); if (repeatPlayList) { logger.debug("a playlist....."); skipTrack(); //Offer it to the queue to prevent the player from playing it final AudioTrack clone = lastTrack.makeClone(); clone.setUserData(createNewTrackData(lastTrack, wasFromSkip)); queue.offer(clone); return; } final AudioTrack clone = lastTrack.makeClone(); clone.setUserData(createNewTrackData(lastTrack, wasFromSkip)); this.player.playTrack(clone); } public boolean isRepeating() { return repeating; } public void setRepeating(boolean repeating) { this.repeating = repeating; } public boolean isRepeatingPlaylists() { return repeatPlayList; } public void setRepeatingPlaylists(boolean repeatingPlaylists) { this.repeatPlayList = repeatingPlaylists; } public void shuffle() { Collections.shuffle((List<?>) queue); } private TrackUserData createNewTrackData(AudioTrack track, boolean wasFromSkip) { final TrackUserData oldData = track.getUserData(TrackUserData.class); final TrackUserData newData; // If we did not have old data (unlikely) we will create it if (oldData == null) { newData = new TrackUserData(0L); } else { newData = oldData.copy(oldData.getRequester()); } // Set the was from skip status on the track newData.setWasFromSkip(wasFromSkip); return newData; } @Override public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { final Throwable rootCause = ExceptionUtils.getRootCause(exception); final Throwable finalCause = rootCause == null ? exception : rootCause; if (finalCause == null || finalCause.getMessage() == null) { this.messageDebouncer.accept("Something went terribly wrong when playing track with identifier `" + track.getIdentifier() + "`\nPlease contact the developers asap with the identifier in the message above"); return; } if (finalCause.getMessage().contains("Something went wrong when decoding the track.")) { return; } this.messageDebouncer.accept("Something went wrong while playing the track, please contact the devs if this happens a lot.\n" + "Details: " + finalCause); // old shit /*if (exception.severity != FriendlyException.Severity.COMMON) { final TextChannel tc = guildMusicManager.getLatestChannel(); final Guild g = tc == null ? null : tc.getGuild(); if (g != null) { final AudioTrackInfo info = track.getInfo(); final String error = String.format( "Guild %s (%s) had an FriendlyException on track \"%s\" by \"%s\" (source %s) (%s)", g.getName(), g.getId(), info.title, info.author, track.getSourceManager().getSourceName(), info.identifier ); logger.error(TextColor.RED + error + TextColor.RESET, exception); } }*/ } }