Skip to content

Commit 67288a0

Browse files
committed
[YouTube] Fix extraction of embeddable age-restricted videos, fix extraction of contents with warnings and more
Use the TV embedded client technique to get streams of embeddable age-restricted videos. This client doesn't provide the playerMicroFormatRenderer object in the player response, but it is still returned on the WEB player response, even for unavailable (but non-private) contents, so we need now to store it, as we are replacing the player response from the WEB client by the TV embedded one. Otherwise, some metadata such as the unlisted property, category, the uploadDate and the publishDate properties. The outdated code for these contents has been removed. Add the racyCheckOk and contentCheckOk to player and next requests to the InnerTube API. The first doesn't seem to make any difference when used anonymously, but the second one is needed to get streams of contents with a warning before they can be played. Also apply some requested changes, fixes and improvements in YoutubeParsingHelper and YoutubeStreamExtractor.
1 parent 11b5a22 commit 67288a0

File tree

2 files changed

+195
-301
lines changed

2 files changed

+195
-301
lines changed

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

Lines changed: 57 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,16 @@ private YoutubeParsingHelper() {
8181
}
8282

8383
/**
84-
* The base URL of requests of the {@code WEB} client to the InnerTube internal API
84+
* The base URL of requests of the {@code WEB} clients to the InnerTube internal API.
8585
*/
8686
public static final String YOUTUBEI_V1_URL = "https://www.youtube.com/youtubei/v1/";
8787

88+
/**
89+
* The base URL of requests of non-web clients to the InnerTube internal API.
90+
*/
91+
public static final String YOUTUBEI_V1_GAPIS_URL =
92+
"https://youtubei.googleapis.com/youtubei/v1/";
93+
8894
/**
8995
* A parameter to disable pretty-printed response of InnerTube requests, to reduce response
9096
* sizes.
@@ -114,6 +120,26 @@ private YoutubeParsingHelper() {
114120
public static final String CPN = "cpn";
115121
public static final String VIDEO_ID = "videoId";
116122

123+
/**
124+
* A parameter sent by official clients named {@code contentCheckOk}.
125+
*
126+
* <p>
127+
* Setting it to {@code true} allows us to get streaming data on videos with a warning about
128+
* what the sensible content they contain.
129+
* </p>
130+
*/
131+
public static final String CONTENT_CHECK_OK = "contentCheckOk";
132+
133+
/**
134+
* A parameter which may be send by official clients named {@code racyCheckOk}.
135+
*
136+
* <p>
137+
* What this parameter does is not really known, but it seems to be linked to sensitive
138+
* contents such as age-restricted content.
139+
* </p>
140+
*/
141+
public static final String RACY_CHECK_OK = "racyCheckOk";
142+
117143
/**
118144
* The client version for InnerTube requests with the {@code WEB} client, used as the last
119145
* fallback if the extraction of the real one failed.
@@ -150,6 +176,12 @@ private YoutubeParsingHelper() {
150176
*/
151177
private static final String MOBILE_YOUTUBE_CLIENT_VERSION = "17.10.35";
152178

179+
/**
180+
* The hardcoded client version of the Android app used for InnerTube requests with this
181+
* client.
182+
*/
183+
private static final String TVHTML5_SIMPLY_EMBED_CLIENT_VERSION = "2.0";
184+
153185
private static String clientVersion;
154186
private static String key;
155187

@@ -664,6 +696,9 @@ public static String getClientVersion() throws IOException, ExtractionException
664696
return clientVersion;
665697
}
666698

699+
// Always extract latest client version, by trying first to extract it from the JavaScript
700+
// service worker, then from HTML search results page as a fallback, to prevent
701+
// fingerprinting based on the client version used
667702
try {
668703
extractClientVersionAndKeyFromSwJs();
669704
} catch (final Exception e) {
@@ -674,6 +709,7 @@ public static String getClientVersion() throws IOException, ExtractionException
674709
return clientVersion;
675710
}
676711

712+
// Fallback to the hardcoded one if it's valid
677713
if (areHardcodedClientVersionAndKeyValid()) {
678714
clientVersion = HARDCODED_CLIENT_VERSION;
679715
return clientVersion;
@@ -690,6 +726,9 @@ public static String getKey() throws IOException, ExtractionException {
690726
return key;
691727
}
692728

729+
// Always extract the key used by the webiste, by trying first to extract it from the
730+
// JavaScript service worker, then from HTML search results page as a fallback, to prevent
731+
// fingerprinting based on the key and/or invalid key issues
693732
try {
694733
extractClientVersionAndKeyFromSwJs();
695734
} catch (final Exception e) {
@@ -700,6 +739,7 @@ public static String getKey() throws IOException, ExtractionException {
700739
return key;
701740
}
702741

742+
// Fallback to the hardcoded one if it's valid
703743
if (areHardcodedClientVersionAndKeyValid()) {
704744
key = HARDCODED_KEY;
705745
return key;
@@ -1058,8 +1098,8 @@ private static JsonObject getMobilePostResponse(
10581098
headers.put("User-Agent", Collections.singletonList(userAgent));
10591099
headers.put("X-Goog-Api-Format-Version", Collections.singletonList("2"));
10601100

1061-
final String baseEndpointUrl = "https://youtubei.googleapis.com/youtubei/v1/" + endpoint
1062-
+ "?key=" + innerTubeApiKey + DISABLE_PRETTY_PRINT_PARAMETER;
1101+
final String baseEndpointUrl = YOUTUBEI_V1_GAPIS_URL + endpoint + "?key=" + innerTubeApiKey
1102+
+ DISABLE_PRETTY_PRINT_PARAMETER;
10631103

10641104
final Response response = getDownloader().post(isNullOrEmpty(endPartOfUrlRequest)
10651105
? baseEndpointUrl : baseEndpointUrl + endPartOfUrlRequest,
@@ -1146,110 +1186,30 @@ public static JsonBuilder<JsonObject> prepareIosMobileJsonBuilder(
11461186
}
11471187

11481188
@Nonnull
1149-
public static JsonBuilder<JsonObject> prepareDesktopEmbedVideoJsonBuilder(
1150-
@Nonnull final Localization localization,
1151-
@Nonnull final ContentCountry contentCountry,
1152-
@Nonnull final String videoId) throws IOException, ExtractionException {
1153-
// @formatter:off
1154-
return JsonObject.builder()
1155-
.object("context")
1156-
.object("client")
1157-
.value("hl", localization.getLocalizationCode())
1158-
.value("gl", contentCountry.getCountryCode())
1159-
.value("clientName", "WEB")
1160-
.value("clientVersion", getClientVersion())
1161-
.value("clientScreen", "EMBED")
1162-
.value("originalUrl", "https://www.youtube.com")
1163-
.value("platform", "DESKTOP")
1164-
.end()
1165-
.object("thirdParty")
1166-
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
1167-
.end()
1168-
.object("request")
1169-
.array("internalExperimentFlags")
1170-
.end()
1171-
.value("useSsl", true)
1172-
.end()
1173-
.object("user")
1174-
// TO DO: provide a way to enable restricted mode with:
1175-
// .value("enableSafetyMode", boolean)
1176-
.value("lockedSafetyMode", false)
1177-
.end()
1178-
.end();
1179-
// @formatter:on
1180-
}
1181-
1182-
@Nonnull
1183-
public static JsonBuilder<JsonObject> prepareAndroidMobileEmbedVideoJsonBuilder(
1184-
@Nonnull final Localization localization,
1185-
@Nonnull final ContentCountry contentCountry,
1186-
@Nonnull final String videoId,
1187-
@Nonnull final String contentPlaybackNonce) {
1188-
// @formatter:off
1189-
return JsonObject.builder()
1190-
.object("context")
1191-
.object("client")
1192-
.value("clientName", "ANDROID")
1193-
.value("clientVersion", MOBILE_YOUTUBE_CLIENT_VERSION)
1194-
.value("clientScreen", "EMBED")
1195-
.value("platform", "MOBILE")
1196-
.value("hl", localization.getLocalizationCode())
1197-
.value("gl", contentCountry.getCountryCode())
1198-
.end()
1199-
.object("thirdParty")
1200-
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
1201-
.end()
1202-
.object("request")
1203-
.array("internalExperimentFlags")
1204-
.end()
1205-
.value("useSsl", true)
1206-
.end()
1207-
.object("user")
1208-
// TO DO: provide a way to enable restricted mode with:
1209-
// .value("enableSafetyMode", boolean)
1210-
.value("lockedSafetyMode", false)
1211-
.end()
1212-
.end()
1213-
.value(CPN, contentPlaybackNonce)
1214-
.value(VIDEO_ID, videoId);
1215-
// @formatter:on
1216-
}
1217-
1218-
@Nonnull
1219-
public static JsonBuilder<JsonObject> prepareIosMobileEmbedVideoJsonBuilder(
1189+
public static JsonBuilder<JsonObject> prepareTvHtml5EmbedJsonBuilder(
12201190
@Nonnull final Localization localization,
12211191
@Nonnull final ContentCountry contentCountry,
1222-
@Nonnull final String videoId,
1223-
@Nonnull final String contentPlaybackNonce) {
1224-
// @formatter:off
1192+
@Nonnull final String videoId) {
1193+
// @formatter:off
12251194
return JsonObject.builder()
12261195
.object("context")
12271196
.object("client")
1228-
.value("clientName", "IOS")
1229-
.value("clientVersion", MOBILE_YOUTUBE_CLIENT_VERSION)
1197+
.value("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER")
1198+
.value("clientVersion", TVHTML5_SIMPLY_EMBED_CLIENT_VERSION)
12301199
.value("clientScreen", "EMBED")
1231-
// Device model is required to get 60fps streams
1232-
.value("deviceModel", IOS_DEVICE_MODEL)
1233-
.value("platform", "MOBILE")
1200+
.value("platform", "TV")
12341201
.value("hl", localization.getLocalizationCode())
12351202
.value("gl", contentCountry.getCountryCode())
12361203
.end()
12371204
.object("thirdParty")
12381205
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
12391206
.end()
1240-
.object("request")
1241-
.array("internalExperimentFlags")
1242-
.end()
1243-
.value("useSsl", true)
1244-
.end()
12451207
.object("user")
12461208
// TO DO: provide a way to enable restricted mode with:
12471209
// .value("enableSafetyMode", boolean)
12481210
.value("lockedSafetyMode", false)
12491211
.end()
1250-
.end()
1251-
.value(CPN, contentPlaybackNonce)
1252-
.value(VIDEO_ID, videoId);
1212+
.end();
12531213
// @formatter:on
12541214
}
12551215

@@ -1259,30 +1219,24 @@ public static byte[] createDesktopPlayerBody(
12591219
@Nonnull final ContentCountry contentCountry,
12601220
@Nonnull final String videoId,
12611221
@Nonnull final String sts,
1262-
final boolean isEmbedClientScreen,
1222+
final boolean isTvHtml5DesktopJsonBuilder,
12631223
@Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
12641224
// @formatter:off
1265-
return JsonWriter.string((isEmbedClientScreen
1266-
? prepareDesktopEmbedVideoJsonBuilder(localization, contentCountry,
1267-
videoId)
1225+
return JsonWriter.string((isTvHtml5DesktopJsonBuilder
1226+
? prepareTvHtml5EmbedJsonBuilder(localization, contentCountry, videoId)
12681227
: prepareDesktopJsonBuilder(localization, contentCountry))
12691228
.object("playbackContext")
12701229
.object("contentPlaybackContext")
1271-
// Some parameters which are sent by the official WEB client (probably some
1272-
// of them are not useful)
1273-
.value("currentUrl", "/watch?v=" + videoId)
1274-
.value("vis", 0)
1275-
.value("splay", false)
1276-
.value("autoCaptionsDefaultOn", false)
1277-
.value("autonavState", "STATE_NONE")
1278-
.value("html5Preference", "HTML5_PREF_WANTS")
1230+
// Some parameters which are sent by the official WEB client in player
1231+
// requests, which seems to avoid throttling on streams from it
12791232
.value("signatureTimestamp", sts)
12801233
.value("referer", "https://www.youtube.com/watch?v=" + videoId)
1281-
.value("lactMilliseconds", "-1")
12821234
.end()
12831235
.end()
12841236
.value(CPN, contentPlaybackNonce)
12851237
.value(VIDEO_ID, videoId)
1238+
.value(CONTENT_CHECK_OK, true)
1239+
.value(RACY_CHECK_OK, true)
12861240
.done())
12871241
.getBytes(StandardCharsets.UTF_8);
12881242
// @formatter:on

0 commit comments

Comments
 (0)