Skip to content

Commit 4c72032

Browse files
authored
Merge pull request #1262 from Theta-Dev/fix/ios-client-vdata
[YouTube] update iOS client, add visitor data to requests
2 parents 8e92227 + 936bf2d commit 4c72032

3 files changed

Lines changed: 126 additions & 16 deletions

File tree

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

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.schabi.newpipe.extractor.stream.AudioTrackType;
4949
import org.schabi.newpipe.extractor.utils.JsonUtils;
5050
import org.schabi.newpipe.extractor.utils.Parser;
51+
import org.schabi.newpipe.extractor.utils.ProtoBuilder;
5152
import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator;
5253
import org.schabi.newpipe.extractor.utils.Utils;
5354

@@ -174,7 +175,7 @@ private YoutubeParsingHelper() {
174175
* Store page of the YouTube app</a>, in the {@code What’s New} section.
175176
* </p>
176177
*/
177-
private static final String IOS_YOUTUBE_CLIENT_VERSION = "19.28.1";
178+
private static final String IOS_YOUTUBE_CLIENT_VERSION = "19.45.4";
178179

179180
/**
180181
* The hardcoded client version used for InnerTube requests with the TV HTML5 embed client.
@@ -222,28 +223,28 @@ private YoutubeParsingHelper() {
222223
private static final String IOS_DEVICE_MODEL = "iPhone16,2";
223224

224225
/**
225-
* Spoofing an iPhone 15 Pro Max running iOS 17.5.1 with the hardcoded version of the iOS app.
226+
* Spoofing an iPhone 15 Pro Max running iOS 18.1.0 with the hardcoded version of the iOS app.
226227
* To be used for the {@code "osVersion"} field in JSON POST requests.
227228
* <p>
228229
* The value of this field seems to use the following structure:
229230
* "iOS major version.minor version.patch version.build version", where
230231
* "patch version" is equal to 0 if it isn't set
231232
* The build version corresponding to the iOS version used can be found on
232-
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15_Pro_Max">
233-
* https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15_Pro_Max</a>
233+
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/18.x#iPhone_15_Pro_Max">
234+
* https://theapplewiki.com/wiki/Firmware/iPhone/18.x#iPhone_15_Pro_Max</a>
234235
* </p>
235236
*
236237
* @see #IOS_USER_AGENT_VERSION
237238
*/
238-
private static final String IOS_OS_VERSION = "17.5.1.21F90";
239+
private static final String IOS_OS_VERSION = "18.1.0.22B83";
239240

240241
/**
241-
* Spoofing an iPhone 15 running iOS 17.5.1 with the hardcoded version of the iOS app. To be
242+
* Spoofing an iPhone 15 Pro Max running iOS 18.1.0 with the hardcoded version of the iOS app. To be
242243
* used in the user agent for requests.
243244
*
244245
* @see #IOS_OS_VERSION
245246
*/
246-
private static final String IOS_USER_AGENT_VERSION = "17_5_1";
247+
private static final String IOS_USER_AGENT_VERSION = "18_1_0";
247248

248249
private static Random numberGenerator = new Random();
249250

@@ -303,6 +304,23 @@ public static boolean isY2ubeURL(@Nonnull final URL url) {
303304
return url.getHost().equalsIgnoreCase("y2u.be");
304305
}
305306

307+
public static String randomVisitorData(final ContentCountry country) {
308+
final ProtoBuilder pbE2 = new ProtoBuilder();
309+
pbE2.string(2, "");
310+
pbE2.varint(4, numberGenerator.nextInt(255) + 1);
311+
312+
final ProtoBuilder pbE = new ProtoBuilder();
313+
pbE.string(1, country.getCountryCode());
314+
pbE.bytes(2, pbE2.toBytes());
315+
316+
final ProtoBuilder pb = new ProtoBuilder();
317+
pb.string(1, RandomStringFromAlphabetGenerator.generate(
318+
CONTENT_PLAYBACK_NONCE_ALPHABET, 11, numberGenerator));
319+
pb.varint(5, System.currentTimeMillis() / 1000 - numberGenerator.nextInt(600000));
320+
pb.bytes(6, pbE.toBytes());
321+
return pb.toUrlencodedBase64();
322+
}
323+
306324
/**
307325
* Parses the duration string of the video expecting ":" or "." as separators
308326
*
@@ -1166,8 +1184,13 @@ public static JsonBuilder<JsonObject> prepareDesktopJsonBuilder(
11661184
@Nonnull final ContentCountry contentCountry,
11671185
@Nullable final String visitorData)
11681186
throws IOException, ExtractionException {
1187+
String vData = visitorData;
1188+
if (vData == null) {
1189+
vData = randomVisitorData(contentCountry);
1190+
}
1191+
11691192
// @formatter:off
1170-
final JsonBuilder<JsonObject> builder = JsonObject.builder()
1193+
return JsonObject.builder()
11711194
.object("context")
11721195
.object("client")
11731196
.value("hl", localization.getLocalizationCode())
@@ -1176,13 +1199,9 @@ public static JsonBuilder<JsonObject> prepareDesktopJsonBuilder(
11761199
.value("clientVersion", getClientVersion())
11771200
.value("originalUrl", "https://www.youtube.com")
11781201
.value("platform", "DESKTOP")
1179-
.value("utcOffsetMinutes", 0);
1180-
1181-
if (visitorData != null) {
1182-
builder.value("visitorData", visitorData);
1183-
}
1184-
1185-
return builder.end()
1202+
.value("utcOffsetMinutes", 0)
1203+
.value("visitorData", vData)
1204+
.end()
11861205
.object("request")
11871206
.array("internalExperimentFlags")
11881207
.end()
@@ -1256,6 +1275,7 @@ public static JsonBuilder<JsonObject> prepareIosMobileJsonBuilder(
12561275
.value("platform", "MOBILE")
12571276
.value("osName", "iOS")
12581277
.value("osVersion", IOS_OS_VERSION)
1278+
.value("visitorData", randomVisitorData(contentCountry))
12591279
.value("hl", localization.getLocalizationCode())
12601280
.value("gl", contentCountry.getCountryCode())
12611281
.value("utcOffsetMinutes", 0)
@@ -1392,7 +1412,7 @@ public static String getAndroidUserAgent(@Nullable final Localization localizati
13921412
*/
13931413
@Nonnull
13941414
public static String getIosUserAgent(@Nullable final Localization localization) {
1395-
// Spoofing an iPhone 15 running iOS 17.5.1 with the hardcoded version of the iOS app
1415+
// Spoofing an iPhone 15 Pro Max running iOS 18.1.0 with the hardcoded version of the iOS app
13961416
return "com.google.ios.youtube/" + IOS_YOUTUBE_CLIENT_VERSION
13971417
+ "(" + IOS_DEVICE_MODEL + "; U; CPU iOS "
13981418
+ IOS_USER_AGENT_VERSION + " like Mac OS X; "
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.schabi.newpipe.extractor.utils;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.net.URLEncoder;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.Base64;
8+
9+
public class ProtoBuilder {
10+
ByteArrayOutputStream byteBuffer;
11+
12+
public ProtoBuilder() {
13+
this.byteBuffer = new ByteArrayOutputStream();
14+
}
15+
16+
public byte[] toBytes() {
17+
return byteBuffer.toByteArray();
18+
}
19+
20+
public String toUrlencodedBase64() {
21+
final String b64 = Base64.getUrlEncoder().encodeToString(toBytes());
22+
return URLEncoder.encode(b64, StandardCharsets.UTF_8);
23+
}
24+
25+
private void writeVarint(final long val) {
26+
try {
27+
if (val == 0) {
28+
byteBuffer.write(new byte[]{(byte) 0});
29+
} else {
30+
long v = val;
31+
while (v != 0) {
32+
byte b = (byte) (v & 0x7f);
33+
v >>= 7;
34+
35+
if (v != 0) {
36+
b |= (byte) 0x80;
37+
}
38+
byteBuffer.write(new byte[]{b});
39+
}
40+
}
41+
} catch (final IOException e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
private void field(final int field, final byte wire) {
47+
final long fbits = ((long) field) << 3;
48+
final long wbits = ((long) wire) & 0x07;
49+
final long val = fbits | wbits;
50+
writeVarint(val);
51+
}
52+
53+
public void varint(final int field, final long val) {
54+
field(field, (byte) 0);
55+
writeVarint(val);
56+
}
57+
58+
public void string(final int field, final String string) {
59+
final byte[] strBts = string.getBytes(StandardCharsets.UTF_8);
60+
bytes(field, strBts);
61+
}
62+
63+
public void bytes(final int field, final byte[] bytes) {
64+
field(field, (byte) 2);
65+
writeVarint(bytes.length);
66+
try {
67+
byteBuffer.write(bytes);
68+
} catch (final IOException e) {
69+
throw new RuntimeException(e);
70+
}
71+
}
72+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.schabi.newpipe.extractor.utils;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
7+
public class ProtoBuilderTest {
8+
@Test
9+
public void testProtoBuilder() {
10+
final ProtoBuilder pb = new ProtoBuilder();
11+
pb.varint(1, 128);
12+
pb.varint(2, 1234567890);
13+
pb.varint(3, 1234567890123456789L);
14+
pb.string(4, "Hello");
15+
pb.bytes(5, new byte[]{1, 2, 3});
16+
assertEquals("CIABENKF2MwEGJWCpu_HnoSRESIFSGVsbG8qAwECAw%3D%3D", pb.toUrlencodedBase64());
17+
}
18+
}

0 commit comments

Comments
 (0)