From 1eb98b7a139c304eddb124a8a1c70860de68fa3d Mon Sep 17 00:00:00 2001 From: kikkia <10608318+kikkia@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:33:46 +0900 Subject: [PATCH 1/4] add all clients failed error --- .../youtube/AllClientsFailedException.java | 18 ++++++ .../dev/lavalink/youtube/ClientException.java | 14 +++++ .../youtube/YoutubeAudioSourceManager.java | 23 ++++---- .../youtube/track/YoutubeAudioTrack.java | 55 ++++--------------- 4 files changed, 56 insertions(+), 54 deletions(-) create mode 100644 common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java create mode 100644 common/src/main/java/dev/lavalink/youtube/ClientException.java 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..5f7ab52 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java @@ -0,0 +1,18 @@ +package dev.lavalink.youtube; + +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; + +import java.util.List; + +/** + * Thrown when all clients failed to load a track. + */ +public class AllClientsFailedException extends FriendlyException { + /** + * @param suppressed The exceptions that were caused client failures. + */ + public AllClientsFailedException(List suppressed) { + super("All clients failed to load the item. See suppressed exceptions for details.", Severity.SUSPICIOUS, null); + suppressed.forEach(this::addSuppressed); + } +} \ 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..b4441df --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/ClientException.java @@ -0,0 +1,14 @@ +package dev.lavalink.youtube; + +import dev.lavalink.youtube.clients.skeleton.Client; +import org.jetbrains.annotations.NotNull; + +/** + * Wraps an exception with client context + */ +public class ClientException extends Exception { + public ClientException(@NotNull String message, @NotNull Client client, @NotNull Throwable cause) { + super(String.format("Client [%s] failed: %s", client.getIdentifier(), message), cause); + addSuppressed(ClientInformation.create(client)); + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java index c9a0eb7..ff03a74 100644 --- a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java +++ b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java @@ -34,6 +34,7 @@ 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; @@ -213,7 +214,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 +245,27 @@ 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 (item == null && !exceptions.isEmpty()) { + throw new AllClientsFailedException(exceptions); } - 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..1683cca 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,16 @@ 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; - } - - 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; - } - } 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; - } - } - - throw e; // Unhandled exception, just rethrow. + return; + } catch (CannotBeLoaded e) { + throw e; + } catch (Exception e) { + 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 ExceptionTools.wrapUnfriendlyExceptions("All clients failed to load the track.", Severity.SUSPICIOUS, new AllClientsFailedException(exceptions)); } } catch (CannotBeLoaded e) { throw ExceptionTools.wrapUnfriendlyExceptions("This video is unavailable", Severity.SUSPICIOUS, e.getCause()); From 9db08d3a38577bd02d4e26f037ae02f5ed1b9cfd Mon Sep 17 00:00:00 2001 From: kikkia <10608318+kikkia@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:53:10 +0900 Subject: [PATCH 2/4] PR comments --- .../youtube/AllClientsFailedException.java | 31 +++++++++++++++-- .../dev/lavalink/youtube/ClientException.java | 33 +++++++++++++++++-- .../lavalink/youtube/ClientInformation.java | 1 - .../youtube/YoutubeAudioSourceManager.java | 13 ++++++-- .../youtube/track/YoutubeAudioTrack.java | 18 ++++++++-- 5 files changed, 85 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java b/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java index 5f7ab52..a146bc5 100644 --- a/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java +++ b/common/src/main/java/dev/lavalink/youtube/AllClientsFailedException.java @@ -1,18 +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(List suppressed) { - super("All clients failed to load the item. See suppressed exceptions for details.", Severity.SUSPICIOUS, null); - suppressed.forEach(this::addSuppressed); + 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 index b4441df..a93ad82 100644 --- a/common/src/main/java/dev/lavalink/youtube/ClientException.java +++ b/common/src/main/java/dev/lavalink/youtube/ClientException.java @@ -3,12 +3,41 @@ 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 Exception { +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); - addSuppressed(ClientInformation.create(client)); + 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(getMessage()); + + Throwable cause = getCause(); + if (cause != null) { + StackTraceElement[] stackTrace = cause.getStackTrace(); + int limit = Math.min(4, stackTrace.length); + for (int i = 0; i < limit; i++) { + printer.println(); + printer.format("\tat %s", stackTrace[i]); + } + } + } + return writer.toString(); } } \ 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 ff03a74..78080ba 100644 --- a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java +++ b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java @@ -39,6 +39,7 @@ 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; @@ -215,7 +216,7 @@ public AudioItem loadItem(@NotNull AudioPlayerManager manager, @NotNull AudioRef @Nullable protected AudioItem loadItemOnce(@NotNull AudioReference reference) { AudioItem item = null; - List exceptions = new ArrayList<>(); + List exceptions = new ArrayList<>(); try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { Router router = getRouter(httpInterface, reference.identifier); @@ -261,8 +262,14 @@ protected AudioItem loadItemOnce(@NotNull AudioReference reference) { throw ExceptionTools.toRuntimeException(e); } - if (item == null && !exceptions.isEmpty()) { - throw new AllClientsFailedException(exceptions); + 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 item; 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 1683cca..52cff22 100644 --- a/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java +++ b/common/src/main/java/dev/lavalink/youtube/track/YoutubeAudioTrack.java @@ -86,7 +86,7 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception { log.debug("Failed to parse token from userData", e); } - List exceptions = new ArrayList<>(); + List exceptions = new ArrayList<>(); for (Client client : clients) { if (!client.supportsFormatLoading()) { @@ -101,12 +101,26 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception { } 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) { + 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) { + throw e; + } + } exceptions.add(new ClientException(e.getMessage(), client, e)); } } if (!exceptions.isEmpty()) { - throw ExceptionTools.wrapUnfriendlyExceptions("All clients failed to load the track.", Severity.SUSPICIOUS, new AllClientsFailedException(exceptions)); + throw new AllClientsFailedException(exceptions); } } catch (CannotBeLoaded e) { throw ExceptionTools.wrapUnfriendlyExceptions("This video is unavailable", Severity.SUSPICIOUS, e.getCause()); From d9697bfa98b62cfcf36b532b4995e45ac6eba971 Mon Sep 17 00:00:00 2001 From: kikkia <10608318+kikkia@users.noreply.github.com> Date: Wed, 22 Oct 2025 23:52:28 +0900 Subject: [PATCH 3/4] recursively go down the causes --- .../dev/lavalink/youtube/ClientException.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/dev/lavalink/youtube/ClientException.java b/common/src/main/java/dev/lavalink/youtube/ClientException.java index a93ad82..ad8ecf1 100644 --- a/common/src/main/java/dev/lavalink/youtube/ClientException.java +++ b/common/src/main/java/dev/lavalink/youtube/ClientException.java @@ -26,18 +26,25 @@ public Client getClient() { public String getFormattedMessage() { StringWriter writer = new StringWriter(); try (PrintWriter printer = new PrintWriter(writer)) { - printer.print(getMessage()); - - Throwable cause = getCause(); - if (cause != null) { - StackTraceElement[] stackTrace = cause.getStackTrace(); - int limit = Math.min(4, stackTrace.length); - for (int i = 0; i < limit; i++) { - printer.println(); - printer.format("\tat %s", stackTrace[i]); - } - } + writeException(printer, this, 3); } return writer.toString(); } + + // Recursively iterate down the causes to our stored exceptions + private void writeException(@NotNull PrintWriter writer, @NotNull Throwable throwable, int maxDepth) { + writer.print(throwable.getMessage()); + StackTraceElement[] stackTrace = throwable.getStackTrace(); + + for (int i = 0; i < Math.min(5, stackTrace.length); i++) { + writer.println(); + writer.format("\tat %s", stackTrace[i]); + } + + if (throwable.getCause() != null && maxDepth > 0) { + writer.println(); + writer.print("Caused by: "); + writeException(writer, throwable.getCause(), maxDepth - 1); + } + } } \ No newline at end of file From 74e0aa69432b2e9f89c58f2cd29d6424e6987fa6 Mon Sep 17 00:00:00 2001 From: kikkia <10608318+kikkia@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:01:25 +0900 Subject: [PATCH 4/4] whoops, start with cause not this exception --- .../dev/lavalink/youtube/ClientException.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/dev/lavalink/youtube/ClientException.java b/common/src/main/java/dev/lavalink/youtube/ClientException.java index ad8ecf1..dab23e4 100644 --- a/common/src/main/java/dev/lavalink/youtube/ClientException.java +++ b/common/src/main/java/dev/lavalink/youtube/ClientException.java @@ -26,25 +26,26 @@ public Client getClient() { public String getFormattedMessage() { StringWriter writer = new StringWriter(); try (PrintWriter printer = new PrintWriter(writer)) { - writeException(printer, this, 3); + 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 writer, @NotNull Throwable throwable, int maxDepth) { - writer.print(throwable.getMessage()); + 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++) { - writer.println(); - writer.format("\tat %s", stackTrace[i]); + printer.println(); + printer.format("\tat %s", stackTrace[i]); } if (throwable.getCause() != null && maxDepth > 0) { - writer.println(); - writer.print("Caused by: "); - writeException(writer, throwable.getCause(), maxDepth - 1); + printer.println(); + printer.print("Caused by: "); + writeException(printer, throwable.getCause(), maxDepth - 1); } } } \ No newline at end of file