Skip to content

Commit 6a885ef

Browse files
authored
Merge pull request #891 from Theta-Dev/fix-throttling-decrypter
[YouTube] Fix extraction of more complex nsig functions
2 parents d120036 + 5b54834 commit 6a885ef

File tree

4 files changed

+111
-31
lines changed

4 files changed

+111
-31
lines changed

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeThrottlingDecrypter.java

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@
1212
import java.util.regex.Pattern;
1313

1414
/**
15+
* YouTube's streaming URLs of HTML5 clients are protected with a cipher, which modifies their
16+
* {@code n} query parameter.
17+
*
1518
* <p>
16-
* YouTube's media is protected with a cipher,
17-
* which modifies the "n" query parameter of it's video playback urls.
18-
* This class handles extracting that "n" query parameter,
19-
* applying the cipher on it and returning the resulting url which is not throttled.
19+
* This class handles extracting that {@code n} query parameter, applying the cipher on it and
20+
* returning the resulting URL which is not throttled.
2021
* </p>
2122
*
22-
* <pre>
23-
* https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=VVF2xyZLVRZZxHXZ&amp;other=other
24-
* </pre>
23+
* <p>
24+
* For instance,
25+
* {@code https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=VVF2xyZLVRZZxHXZ&other=other}
2526
* becomes
26-
* <pre>
27-
* https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=iHywZkMipkszqA&amp;other=other
28-
* </pre>
29-
* <br>
27+
* {@code https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=iHywZkMipkszqA&other=other}.
28+
* </p>
29+
*
3030
* <p>
31-
* Decoding the "n" parameter is time intensive. For this reason, the results are cached.
32-
* The cache can be cleared using {@link #clearCache()}
31+
* Decoding the {@code n} parameter is time intensive. For this reason, the results are cached.
32+
* The cache can be cleared using {@link #clearCache()}.
3333
* </p>
3434
*
3535
*/
@@ -73,13 +73,35 @@ public YoutubeThrottlingDecrypter() throws ParsingException {
7373
}
7474

7575
/**
76+
* Try to decrypt a YouTube streaming URL protected with a throttling parameter.
77+
*
7678
* <p>
77-
* The videoId is only used to fetch the decryption function.
78-
* It can be a constant value of any existing video.
79-
* A constant value is discouraged, because it could allow tracking.
79+
* If the streaming URL provided doesn't contain a throttling parameter, it is returned as it
80+
* is; otherwise, the encrypted value is decrypted and this value is replaced by the decrypted
81+
* one.
82+
* </p>
83+
*
84+
* <p>
85+
* If the JavaScript code has been not extracted, it is extracted with the given video ID using
86+
* {@link YoutubeJavaScriptExtractor#extractJavaScriptCode(String)}.
87+
* </p>
88+
*
89+
* @param streamingUrl The streaming URL to decrypt, if needed.
90+
* @param videoId A video ID, used to fetch the JavaScript code to get the decryption
91+
* function. It can be a constant value of any existing video, but a
92+
* constant value is discouraged, because it could allow tracking.
93+
* @return A streaming URL with the decrypted parameter or the streaming URL itself if no
94+
* throttling parameter has been found
95+
* @throws ParsingException If the streaming URL contains a throttling parameter and its
96+
* decryption failed
8097
*/
81-
public static String apply(final String url, final String videoId) throws ParsingException {
82-
if (containsNParam(url)) {
98+
public static String apply(@Nonnull final String streamingUrl,
99+
@Nonnull final String videoId) throws ParsingException {
100+
if (!containsNParam(streamingUrl)) {
101+
return streamingUrl;
102+
}
103+
104+
try {
83105
if (FUNCTION == null) {
84106
final String playerJsCode
85107
= YoutubeJavaScriptExtractor.extractJavaScriptCode(videoId);
@@ -88,11 +110,11 @@ public static String apply(final String url, final String videoId) throws Parsin
88110
FUNCTION = parseDecodeFunction(playerJsCode, FUNCTION_NAME);
89111
}
90112

91-
final String oldNParam = parseNParam(url);
113+
final String oldNParam = parseNParam(streamingUrl);
92114
final String newNParam = decryptNParam(FUNCTION, FUNCTION_NAME, oldNParam);
93-
return replaceNParam(url, oldNParam, newNParam);
94-
} else {
95-
return url;
115+
return replaceNParam(streamingUrl, oldNParam, newNParam);
116+
} catch (final Exception e) {
117+
throw new ParsingException("Could not parse, decrypt or replace n parameter", e);
96118
}
97119
}
98120

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -602,15 +602,21 @@ public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
602602
}
603603

604604
/**
605-
* Try to decrypt url and fallback to given url, because decryption is not
606-
* always needed.
605+
* Try to decrypt a streaming URL and fallback to the given URL, because decryption may fail if
606+
* YouTube do breaking changes.
607+
*
608+
* <p>
607609
* This way a breaking change from YouTube does not result in a broken extractor.
610+
* </p>
611+
*
612+
* @param streamingUrl the streaming URL to decrypt with {@link YoutubeThrottlingDecrypter}
613+
* @param videoId the video ID to use when extracting JavaScript player code, if needed
608614
*/
609-
private String tryDecryptUrl(final String url, final String videoId) {
615+
private String tryDecryptUrl(final String streamingUrl, final String videoId) {
610616
try {
611-
return YoutubeThrottlingDecrypter.apply(url, videoId);
617+
return YoutubeThrottlingDecrypter.apply(streamingUrl, videoId);
612618
} catch (final ParsingException e) {
613-
return url;
619+
return streamingUrl;
614620
}
615621
}
616622

extractor/src/main/java/org/schabi/newpipe/extractor/utils/StringUtils.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ public static String matchToClosingParenthesis(@Nonnull final String string,
2323
}
2424

2525
startIndex += start.length();
26-
int endIndex = startIndex;
27-
while (string.charAt(endIndex) != '{') {
28-
++endIndex;
29-
}
26+
int endIndex = findNextParenthesis(string, startIndex, true);
3027
++endIndex;
3128

3229
int openParenthesis = 1;
3330
while (openParenthesis > 0) {
31+
endIndex = findNextParenthesis(string, endIndex, false);
32+
3433
switch (string.charAt(endIndex)) {
3534
case '{':
3635
++openParenthesis;
@@ -46,4 +45,47 @@ public static String matchToClosingParenthesis(@Nonnull final String string,
4645

4746
return string.substring(startIndex, endIndex);
4847
}
48+
49+
private static int findNextParenthesis(@Nonnull final String string,
50+
final int offset,
51+
final boolean onlyOpen) {
52+
boolean lastEscaped = false;
53+
char quote = ' ';
54+
55+
for (int i = offset; i < string.length(); i++) {
56+
boolean thisEscaped = false;
57+
final char c = string.charAt(i);
58+
59+
switch (c) {
60+
case '{':
61+
if (quote == ' ') {
62+
return i;
63+
}
64+
break;
65+
case '}':
66+
if (!onlyOpen && quote == ' ') {
67+
return i;
68+
}
69+
break;
70+
case '\\':
71+
if (!lastEscaped) {
72+
thisEscaped = true;
73+
}
74+
break;
75+
case '\'':
76+
case '"':
77+
if (!lastEscaped) {
78+
if (quote == ' ') {
79+
quote = c;
80+
} else if (quote == c) {
81+
quote = ' ';
82+
}
83+
}
84+
}
85+
86+
lastEscaped = thisEscaped;
87+
}
88+
89+
return -1;
90+
}
4991
}

extractor/src/test/java/org/schabi/newpipe/extractor/utils/StringUtilsTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,14 @@ public void lessClosing__success() {
5858

5959
assertEquals(expected, substring);
6060
}
61+
62+
@Test
63+
void find_closing_with_quotes() {
64+
final String expected = "{return \",}\\\"/\"}";
65+
final String string = "function(d){return \",}\\\"/\"}";
66+
67+
final String substring = matchToClosingParenthesis(string, "function(d)");
68+
69+
assertEquals(expected, substring);
70+
}
6171
}

0 commit comments

Comments
 (0)