Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ClientException> clientExceptions;

/**
* @param suppressed The exceptions that were caused client failures.
*/
public AllClientsFailedException(@NotNull List<ClientException> suppressed) {
super(createMessage(suppressed), Severity.SUSPICIOUS, null);
this.clientExceptions = suppressed;
}

@NotNull
public List<ClientException> getClientExceptions() {
return clientExceptions;
}

private static String createMessage(@NotNull List<ClientException> 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();
}
}
51 changes: 51 additions & 0 deletions common/src/main/java/dev/lavalink/youtube/ClientException.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<ClientException> exceptions = new ArrayList<>();

try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) {
Router router = getRouter(httpInterface, reference.identifier);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -84,7 +86,7 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception {
log.debug("Failed to parse token from userData", e);
}

Exception lastException = null;
List<ClientException> exceptions = new ArrayList<>();

for (Client client : clients) {
if (!client.supportsFormatLoading()) {
Expand All @@ -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());
Expand Down