Merge pull request #891 from Theta-Dev/fix-throttling-decrypter
[YouTube] Fix extraction of more complex nsig functions
This commit is contained in:
commit
6a885ef5ab
4 changed files with 112 additions and 32 deletions
|
@ -12,24 +12,24 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* YouTube's streaming URLs of HTML5 clients are protected with a cipher, which modifies their
|
||||||
|
* {@code n} query parameter.
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* YouTube's media is protected with a cipher,
|
* This class handles extracting that {@code n} query parameter, applying the cipher on it and
|
||||||
* which modifies the "n" query parameter of it's video playback urls.
|
* returning the resulting URL which is not throttled.
|
||||||
* This class handles extracting that "n" query parameter,
|
|
||||||
* applying the cipher on it and returning the resulting url which is not throttled.
|
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <pre>
|
|
||||||
* https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=VVF2xyZLVRZZxHXZ&other=other
|
|
||||||
* </pre>
|
|
||||||
* becomes
|
|
||||||
* <pre>
|
|
||||||
* https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=iHywZkMipkszqA&other=other
|
|
||||||
* </pre>
|
|
||||||
* <br>
|
|
||||||
* <p>
|
* <p>
|
||||||
* Decoding the "n" parameter is time intensive. For this reason, the results are cached.
|
* For instance,
|
||||||
* The cache can be cleared using {@link #clearCache()}
|
* {@code https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=VVF2xyZLVRZZxHXZ&other=other}
|
||||||
|
* becomes
|
||||||
|
* {@code https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=iHywZkMipkszqA&other=other}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Decoding the {@code n} parameter is time intensive. For this reason, the results are cached.
|
||||||
|
* The cache can be cleared using {@link #clearCache()}.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -73,13 +73,35 @@ public class YoutubeThrottlingDecrypter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Try to decrypt a YouTube streaming URL protected with a throttling parameter.
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The videoId is only used to fetch the decryption function.
|
* If the streaming URL provided doesn't contain a throttling parameter, it is returned as it
|
||||||
* It can be a constant value of any existing video.
|
* is; otherwise, the encrypted value is decrypted and this value is replaced by the decrypted
|
||||||
* A constant value is discouraged, because it could allow tracking.
|
* one.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the JavaScript code has been not extracted, it is extracted with the given video ID using
|
||||||
|
* {@link YoutubeJavaScriptExtractor#extractJavaScriptCode(String)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param streamingUrl The streaming URL to decrypt, if needed.
|
||||||
|
* @param videoId A video ID, used to fetch the JavaScript code to get the decryption
|
||||||
|
* function. It can be a constant value of any existing video, but a
|
||||||
|
* constant value is discouraged, because it could allow tracking.
|
||||||
|
* @return A streaming URL with the decrypted parameter or the streaming URL itself if no
|
||||||
|
* throttling parameter has been found
|
||||||
|
* @throws ParsingException If the streaming URL contains a throttling parameter and its
|
||||||
|
* decryption failed
|
||||||
*/
|
*/
|
||||||
public static String apply(final String url, final String videoId) throws ParsingException {
|
public static String apply(@Nonnull final String streamingUrl,
|
||||||
if (containsNParam(url)) {
|
@Nonnull final String videoId) throws ParsingException {
|
||||||
|
if (!containsNParam(streamingUrl)) {
|
||||||
|
return streamingUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (FUNCTION == null) {
|
if (FUNCTION == null) {
|
||||||
final String playerJsCode
|
final String playerJsCode
|
||||||
= YoutubeJavaScriptExtractor.extractJavaScriptCode(videoId);
|
= YoutubeJavaScriptExtractor.extractJavaScriptCode(videoId);
|
||||||
|
@ -88,11 +110,11 @@ public class YoutubeThrottlingDecrypter {
|
||||||
FUNCTION = parseDecodeFunction(playerJsCode, FUNCTION_NAME);
|
FUNCTION = parseDecodeFunction(playerJsCode, FUNCTION_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String oldNParam = parseNParam(url);
|
final String oldNParam = parseNParam(streamingUrl);
|
||||||
final String newNParam = decryptNParam(FUNCTION, FUNCTION_NAME, oldNParam);
|
final String newNParam = decryptNParam(FUNCTION, FUNCTION_NAME, oldNParam);
|
||||||
return replaceNParam(url, oldNParam, newNParam);
|
return replaceNParam(streamingUrl, oldNParam, newNParam);
|
||||||
} else {
|
} catch (final Exception e) {
|
||||||
return url;
|
throw new ParsingException("Could not parse, decrypt or replace n parameter", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -602,15 +602,21 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to decrypt url and fallback to given url, because decryption is not
|
* Try to decrypt a streaming URL and fallback to the given URL, because decryption may fail if
|
||||||
* always needed.
|
* YouTube do breaking changes.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
* This way a breaking change from YouTube does not result in a broken extractor.
|
* This way a breaking change from YouTube does not result in a broken extractor.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param streamingUrl the streaming URL to decrypt with {@link YoutubeThrottlingDecrypter}
|
||||||
|
* @param videoId the video ID to use when extracting JavaScript player code, if needed
|
||||||
*/
|
*/
|
||||||
private String tryDecryptUrl(final String url, final String videoId) {
|
private String tryDecryptUrl(final String streamingUrl, final String videoId) {
|
||||||
try {
|
try {
|
||||||
return YoutubeThrottlingDecrypter.apply(url, videoId);
|
return YoutubeThrottlingDecrypter.apply(streamingUrl, videoId);
|
||||||
} catch (final ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
return url;
|
return streamingUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,13 @@ public final class StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
startIndex += start.length();
|
startIndex += start.length();
|
||||||
int endIndex = startIndex;
|
int endIndex = findNextParenthesis(string, startIndex, true);
|
||||||
while (string.charAt(endIndex) != '{') {
|
|
||||||
++endIndex;
|
|
||||||
}
|
|
||||||
++endIndex;
|
++endIndex;
|
||||||
|
|
||||||
int openParenthesis = 1;
|
int openParenthesis = 1;
|
||||||
while (openParenthesis > 0) {
|
while (openParenthesis > 0) {
|
||||||
|
endIndex = findNextParenthesis(string, endIndex, false);
|
||||||
|
|
||||||
switch (string.charAt(endIndex)) {
|
switch (string.charAt(endIndex)) {
|
||||||
case '{':
|
case '{':
|
||||||
++openParenthesis;
|
++openParenthesis;
|
||||||
|
@ -46,4 +45,47 @@ public final class StringUtils {
|
||||||
|
|
||||||
return string.substring(startIndex, endIndex);
|
return string.substring(startIndex, endIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int findNextParenthesis(@Nonnull final String string,
|
||||||
|
final int offset,
|
||||||
|
final boolean onlyOpen) {
|
||||||
|
boolean lastEscaped = false;
|
||||||
|
char quote = ' ';
|
||||||
|
|
||||||
|
for (int i = offset; i < string.length(); i++) {
|
||||||
|
boolean thisEscaped = false;
|
||||||
|
final char c = string.charAt(i);
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case '{':
|
||||||
|
if (quote == ' ') {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
if (!onlyOpen && quote == ' ') {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
if (!lastEscaped) {
|
||||||
|
thisEscaped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '\'':
|
||||||
|
case '"':
|
||||||
|
if (!lastEscaped) {
|
||||||
|
if (quote == ' ') {
|
||||||
|
quote = c;
|
||||||
|
} else if (quote == c) {
|
||||||
|
quote = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEscaped = thisEscaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,4 +58,14 @@ public class StringUtilsTest {
|
||||||
|
|
||||||
assertEquals(expected, substring);
|
assertEquals(expected, substring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void find_closing_with_quotes() {
|
||||||
|
final String expected = "{return \",}\\\"/\"}";
|
||||||
|
final String string = "function(d){return \",}\\\"/\"}";
|
||||||
|
|
||||||
|
final String substring = matchToClosingParenthesis(string, "function(d)");
|
||||||
|
|
||||||
|
assertEquals(expected, substring);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue