diff --git a/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java b/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java new file mode 100644 index 0000000..a146bc5 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java @@ -0,0 +1,43 @@ +package dev.lavalink.youtube; + +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import org.jetbrains.annotations.NotNull; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +/** + * Thrown when all clients failed to load a track. + */ +public class AllClientsFailedException extends FriendlyException { + private final List clientExceptions; + + /** + * @param suppressed The exceptions that were caused client failures. + */ + public AllClientsFailedException(@NotNull List suppressed) { + super(createMessage(suppressed), Severity.SUSPICIOUS, null); + this.clientExceptions = suppressed; + } + + @NotNull + public List getClientExceptions() { + return clientExceptions; + } + + private static String createMessage(@NotNull List exceptions) { + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + + printer.format("(yts.version: %s) All clients failed to load the item.", YoutubeSource.VERSION); + + for (ClientException exception : exceptions) { + printer.println(); + printer.println(); + printer.print(exception.getFormattedMessage()); + } + + return writer.toString(); + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/lavalink/youtube/ClientException.java b/common/src/main/java/dev/lavalink/youtube/ClientException.java new file mode 100644 index 0000000..dab23e4 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/ClientException.java @@ -0,0 +1,51 @@ +package dev.lavalink.youtube; + +import dev.lavalink.youtube.clients.skeleton.Client; +import org.jetbrains.annotations.NotNull; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Wraps an exception with client context + */ +public class ClientException extends RuntimeException { + private final Client client; + + public ClientException(@NotNull String message, @NotNull Client client, @NotNull Throwable cause) { + super(String.format("Client [%s] failed: %s", client.getIdentifier(), message), cause); + this.client = client; + } + + @NotNull + public Client getClient() { + return client; + } + + @NotNull + public String getFormattedMessage() { + StringWriter writer = new StringWriter(); + try (PrintWriter printer = new PrintWriter(writer)) { + printer.print(this.getMessage()); + writeException(printer, this.getCause(), 3); + } + return writer.toString(); + } + + // Recursively iterate down the causes to our stored exceptions + private void writeException(@NotNull PrintWriter printer, @NotNull Throwable throwable, int maxDepth) { + printer.print(throwable.getMessage()); + StackTraceElement[] stackTrace = throwable.getStackTrace(); + + for (int i = 0; i < Math.min(5, stackTrace.length); i++) { + printer.println(); + printer.format("\tat %s", stackTrace[i]); + } + + if (throwable.getCause() != null && maxDepth > 0) { + printer.println(); + printer.print("Caused by: "); + writeException(printer, throwable.getCause(), maxDepth - 1); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/lavalink/youtube/ClientInformation.java b/common/src/main/java/dev/lavalink/youtube/ClientInformation.java index 8adeb35..1303446 100644 --- a/common/src/main/java/dev/lavalink/youtube/ClientInformation.java +++ b/common/src/main/java/dev/lavalink/youtube/ClientInformation.java @@ -10,7 +10,6 @@ private ClientInformation(String message) { public static ClientInformation create(Client client) { DetailMessageBuilder builder = new DetailMessageBuilder(); - builder.appendField("yts.version", YoutubeSource.VERSION); builder.appendField("client.identifier", client.getIdentifier()); builder.appendField("client.options", client.getOptions()); return new ClientInformation(builder.toString()); diff --git a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java index c9a0eb7..78080ba 100644 --- a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java +++ b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java @@ -34,10 +34,12 @@ import java.io.DataOutput; import java.io.IOException; import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; @@ -213,7 +215,8 @@ public AudioItem loadItem(@NotNull AudioPlayerManager manager, @NotNull AudioRef @Nullable protected AudioItem loadItemOnce(@NotNull AudioReference reference) { - Throwable lastException = null; + AudioItem item = null; + List exceptions = new ArrayList<>(); try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { Router router = getRouter(httpInterface, reference.identifier); @@ -243,28 +246,33 @@ protected AudioItem loadItemOnce(@NotNull AudioReference reference) { httpInterface.getContext().setAttribute(Client.OAUTH_CLIENT_ATTRIBUTE, client.supportsOAuth()); try { - AudioItem item = router.route(client); - - if (item != null) { - return item; - } + item = router.route(client); } catch (CannotBeLoaded cbl) { throw ExceptionTools.wrapUnfriendlyExceptions("This video cannot be loaded.", Severity.SUSPICIOUS, cbl.getCause()); } catch (Throwable t) { log.debug("Client \"{}\" threw a non-fatal exception, storing and proceeding...", client.getIdentifier(), t); - t.addSuppressed(ClientInformation.create(client)); - lastException = t; + exceptions.add(new ClientException(t.getMessage(), client, t)); + } + + if (item != null) { + break; } } } catch (IOException e) { throw ExceptionTools.toRuntimeException(e); } - if (lastException != null) { - throw ExceptionTools.wrapUnfriendlyExceptions("This video cannot be loaded.", SUSPICIOUS, lastException); + if (!exceptions.isEmpty()) { + if (item == null) { + throw new AllClientsFailedException(exceptions); + } + + String exceptionSummary = exceptions.stream().map(ClientException::getFormattedMessage).collect(Collectors.toList()).toString(); + + log.debug("Exceptions suppressed whilst loading {}: {}", reference.identifier, exceptionSummary); } - return null; + return item; } @Nullable diff --git a/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java b/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java index 8f557ad..52cff22 100644 --- a/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java +++ b/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java @@ -13,10 +13,10 @@ import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; import dev.lavalink.youtube.CannotBeLoaded; +import dev.lavalink.youtube.AllClientsFailedException; import dev.lavalink.youtube.ClientInformation; -import dev.lavalink.youtube.UrlTools; +import dev.lavalink.youtube.*; import dev.lavalink.youtube.UrlTools.UrlInfo; -import dev.lavalink.youtube.YoutubeAudioSourceManager; import dev.lavalink.youtube.cipher.ScriptExtractionException; import dev.lavalink.youtube.clients.skeleton.Client; import dev.lavalink.youtube.track.format.StreamFormat; @@ -29,7 +29,9 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import static com.sedmelluq.discord.lavaplayer.container.Formats.MIME_AUDIO_WEBM; @@ -84,7 +86,7 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception { log.debug("Failed to parse token from userData", e); } - Exception lastException = null; + List exceptions = new ArrayList<>(); for (Client client : clients) { if (!client.supportsFormatLoading()) { @@ -95,49 +97,30 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception { try { processWithClient(localExecutor, httpInterface, client, 0); - return; // stream played through successfully, short-circuit. - } catch (RuntimeException e) { - // store exception so it can be thrown if we run out of clients to - // load formats with. - e.addSuppressed(ClientInformation.create(client)); - lastException = e; - - if (e instanceof FriendlyException) { - // usually thrown by getPlayabilityStatus when loading formats. - // these aren't considered fatal, so we just store them and continue. - continue; - } - + return; + } catch (CannotBeLoaded e) { + throw e; + } catch (Exception e) { if (e instanceof ScriptExtractionException) { // If we're still early in playback, we can try another client - if (localExecutor.getPosition() <= BAD_STREAM_POSITION_THRESHOLD_MS) { - continue; + if (localExecutor.getPosition() >= BAD_STREAM_POSITION_THRESHOLD_MS) { + throw e; } } else if ("Not success status code: 403".equals(e.getMessage()) || "Invalid status code for player api response: 400".equals(e.getMessage())) { // As long as the executor position has not surpassed the threshold for which // a stream is considered unrecoverable, we can try to renew the playback URL with // another client. - if (localExecutor.getPosition() <= BAD_STREAM_POSITION_THRESHOLD_MS) { - continue; + if (localExecutor.getPosition() >= BAD_STREAM_POSITION_THRESHOLD_MS) { + throw e; } } - - throw e; // Unhandled exception, just rethrow. + exceptions.add(new ClientException(e.getMessage(), client, e)); } } - if (lastException != null) { - if (lastException instanceof FriendlyException) { - if (!"YouTube WebM streams are currently not supported.".equals(lastException.getMessage())) { - // Rethrow certain FriendlyExceptions as suspicious to ensure LavaPlayer logs them. - throw new FriendlyException(lastException.getMessage(), Severity.SUSPICIOUS, lastException.getCause()); - } - - throw lastException; - } - - throw ExceptionTools.toRuntimeException(lastException); + if (!exceptions.isEmpty()) { + throw new AllClientsFailedException(exceptions); } } catch (CannotBeLoaded e) { throw ExceptionTools.wrapUnfriendlyExceptions("This video is unavailable", Severity.SUSPICIOUS, e.getCause());