diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java index 8a81bd4b2..ec22f90cc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java @@ -6,8 +6,6 @@ import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import java.io.IOException; -import java.net.URLEncoder; import org.apache.http.NameValuePair; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -18,6 +16,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.net.URLEncoder; + import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackJsonData.fromEmbedParts; import static com.sedmelluq.discord.lavaplayer.tools.ExceptionTools.throwWithDebugInfo; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; @@ -297,13 +298,25 @@ protected YoutubeTrackJsonData augmentWithPlayerScript( } } - protected static class CachedPlayerScript { - public final String playerScriptUrl; - public final long timestamp; + public CachedPlayerScript getCachedPlayerScript() { + return cachedPlayerScript; + } + + public void clearCache() { + cachedPlayerScript = null; + } + + public static class CachedPlayerScript { + private final String playerScriptUrl; + private final long timestamp; public CachedPlayerScript(String playerScriptUrl, long timestamp) { this.playerScriptUrl = playerScriptUrl; this.timestamp = timestamp; } + + public String getPlayerScriptUrl() { + return playerScriptUrl; + } } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java index 9173293f6..bf6c20692 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java @@ -2,6 +2,13 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -18,12 +25,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Handles parsing and caching of signature ciphers @@ -235,6 +236,10 @@ private YoutubeSignatureCipher extractTokensFromScript(String script, String sou return cipherKey; } + public void clearCache(String cipherScriptUrl) { + cipherCache.remove(cipherScriptUrl); + } + private static String extractDollarEscapedFirstGroup(Pattern pattern, String text) { Matcher matcher = pattern.matcher(text); return matcher.find() ? matcher.group(1).replace("$", "\\$") : null; diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java index 41ab10d56..28eb79669 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java @@ -3,6 +3,9 @@ import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat; import com.sedmelluq.discord.lavaplayer.player.AudioConfiguration; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerOptions; +import com.sedmelluq.discord.lavaplayer.source.youtube.DefaultYoutubeTrackDetailsLoader; +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeSignatureCipherManager; import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrackState; @@ -30,6 +33,7 @@ */ public class LocalAudioTrackExecutor implements AudioTrackExecutor { private static final Logger log = LoggerFactory.getLogger(LocalAudioTrackExecutor.class); + private static final long RETRY_COOLDOWN = 5000L; private final InternalAudioTrack audioTrack; private final AudioProcessingContext processingContext; @@ -45,6 +49,7 @@ public class LocalAudioTrackExecutor implements AudioTrackExecutor { private long externalSeekPosition = -1; private boolean interruptibleForSeek = false; private volatile Throwable trackException; + private volatile long lastRetry = -1; /** * @param audioTrack The audio track that this executor executes @@ -89,7 +94,6 @@ public AudioFrameBuffer getAudioBuffer() { @Override public void execute(TrackStateListener listener) { - InterruptedException interrupt = null; if (Thread.interrupted()) { log.debug("Cleared a stray interrupt."); @@ -99,44 +103,75 @@ public void execute(TrackStateListener listener) { log.debug("Starting to play track {} locally with listener {}", audioTrack.getInfo().identifier, listener); state.set(AudioTrackState.LOADING); + attemptProcess(listener); - try { - audioTrack.process(this); + } else { + log.warn("Tried to start an already playing track {}", audioTrack.getIdentifier()); + } + } - log.debug("Playing track {} finished or was stopped.", audioTrack.getIdentifier()); - } catch (Throwable e) { - // Temporarily clear the interrupted status so it would not disrupt listener methods. - interrupt = findInterrupt(e); + private void attemptProcess(TrackStateListener listener) { - if (interrupt != null && checkStopped()) { - log.debug("Track {} was interrupted outside of execution loop.", audioTrack.getIdentifier()); - } else { - frameBuffer.setTerminateOnEmpty(); + InterruptedException interrupt = null; - FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something broke when playing the track.", FAULT, e); - ExceptionTools.log(log, exception, "playback of " + audioTrack.getIdentifier()); + try { + audioTrack.process(this); - trackException = exception; - listener.onTrackException(audioTrack, exception); + log.debug("Playing track {} finished or was stopped.", audioTrack.getIdentifier()); + } catch (Throwable e) { - ExceptionTools.rethrowErrors(e); - } - } finally { - synchronized (actionSynchronizer) { - interrupt = interrupt != null ? interrupt : findInterrupt(null); + // Check for 403, attempt to clear cipher cache and retry if no retries in the past 5 seconds. + if (e.getMessage().contains("Not success status code: 403") + && (lastRetry == -1 || lastRetry + RETRY_COOLDOWN <= System.currentTimeMillis()) + && audioTrack.getSourceManager() instanceof YoutubeAudioSourceManager) { + lastRetry = System.currentTimeMillis(); - playingThread.compareAndSet(Thread.currentThread(), null); + log.debug("Detected 403, clearing cipher cache and retrying."); - markerTracker.trigger(ENDED); - state.set(AudioTrackState.FINISHED); - } + YoutubeAudioSourceManager sourceManager = (YoutubeAudioSourceManager) audioTrack.getSourceManager(); + DefaultYoutubeTrackDetailsLoader trackDetailsLoader = (DefaultYoutubeTrackDetailsLoader) sourceManager.getTrackDetailsLoader(); + DefaultYoutubeTrackDetailsLoader.CachedPlayerScript cachedScript = trackDetailsLoader.getCachedPlayerScript(); - if (interrupt != null) { - Thread.currentThread().interrupt(); + // Clear cached scripts and ciphers. + if (cachedScript != null) { + ((YoutubeSignatureCipherManager) sourceManager.getSignatureResolver()).clearCache(cachedScript.getPlayerScriptUrl()); + trackDetailsLoader.clearCache(); } + + // Attempt to process again. + attemptProcess(listener); + return; + } + + // Temporarily clear the interrupted status so it would not disrupt listener methods. + interrupt = findInterrupt(e); + + if (interrupt != null && checkStopped()) { + log.debug("Track {} was interrupted outside of execution loop.", audioTrack.getIdentifier()); + } else { + frameBuffer.setTerminateOnEmpty(); + + FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something broke when playing the track.", FAULT, e); + ExceptionTools.log(log, exception, "playback of " + audioTrack.getIdentifier()); + + trackException = exception; + listener.onTrackException(audioTrack, exception); + + ExceptionTools.rethrowErrors(e); + } + } finally { + synchronized (actionSynchronizer) { + interrupt = interrupt != null ? interrupt : findInterrupt(null); + + playingThread.compareAndSet(Thread.currentThread(), null); + + markerTracker.trigger(ENDED); + state.set(AudioTrackState.FINISHED); + } + + if (interrupt != null) { + Thread.currentThread().interrupt(); } - } else { - log.warn("Tried to start an already playing track {}", audioTrack.getIdentifier()); } }