1515import com .grack .nanojson .JsonWriter ;
1616
1717import org .schabi .newpipe .extractor .MetaInfo ;
18- import org .schabi .newpipe .extractor .Page ;
1918import org .schabi .newpipe .extractor .downloader .Response ;
2019import org .schabi .newpipe .extractor .exceptions .AccountTerminatedException ;
2120import org .schabi .newpipe .extractor .exceptions .ContentNotAvailableException ;
3534import java .net .MalformedURLException ;
3635import java .net .URL ;
3736import java .net .URLDecoder ;
37+ import java .nio .charset .StandardCharsets ;
38+ import java .security .SecureRandom ;
3839import java .time .LocalDate ;
3940import java .time .OffsetDateTime ;
4041import java .time .ZoneOffset ;
@@ -78,11 +79,15 @@ private YoutubeParsingHelper() {
7879 }
7980
8081 public static final String YOUTUBEI_V1_URL = "https://www.youtube.com/youtubei/v1/" ;
82+ public static final String CPN = "cpn" ;
83+ public static final String VIDEO_ID = "videoId" ;
8184
8285 private static final String HARDCODED_CLIENT_VERSION = "2.20220107.00.00" ;
8386 private static final String HARDCODED_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" ;
84- private static final String MOBILE_YOUTUBE_KEY = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w" ;
87+
88+ private static final String ANDROID_YOUTUBE_KEY = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w" ;
8589 private static final String MOBILE_YOUTUBE_CLIENT_VERSION = "16.49.37" ;
90+
8691 private static String clientVersion ;
8792 private static String key ;
8893
@@ -94,6 +99,9 @@ private YoutubeParsingHelper() {
9499 @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
95100 private static Optional <Boolean > hardcodedClientVersionAndKeyValid = Optional .empty ();
96101
102+ private static final String CONTENT_PLAYBACK_NONCE_ALPHABET =
103+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" ;
104+
97105 private static Random numberGenerator = new Random ();
98106
99107 /**
@@ -593,7 +601,7 @@ public static String getKey() throws IOException, ExtractionException {
593601
594602 // The ANDROID API key is also valid with the WEB client so return it if we couldn't
595603 // extract the WEB API key.
596- return MOBILE_YOUTUBE_KEY ;
604+ return ANDROID_YOUTUBE_KEY ;
597605 }
598606
599607 /**
@@ -769,7 +777,7 @@ public static String getUrlFromNavigationEndpoint(@Nonnull final JsonObject navi
769777 } else if (navigationEndpoint .has ("watchEndpoint" )) {
770778 final StringBuilder url = new StringBuilder ();
771779 url .append ("https://www.youtube.com/watch?v=" ).append (navigationEndpoint
772- .getObject ("watchEndpoint" ).getString ("videoId" ));
780+ .getObject ("watchEndpoint" ).getString (VIDEO_ID ));
773781 if (navigationEndpoint .getObject ("watchEndpoint" ).has ("playlistId" )) {
774782 url .append ("&list=" ).append (navigationEndpoint .getObject ("watchEndpoint" )
775783 .getString ("playlistId" ));
@@ -906,17 +914,6 @@ public static String getValidJsonResponseBody(@Nonnull final Response response)
906914 return responseBody ;
907915 }
908916
909- public static Response getResponse (final String url , final Localization localization )
910- throws IOException , ExtractionException {
911- final Map <String , List <String >> headers = new HashMap <>();
912- addYouTubeHeaders (headers );
913-
914- final Response response = getDownloader ().get (url , headers , localization );
915- getValidJsonResponseBody (response );
916-
917- return response ;
918- }
919-
920917 public static JsonObject getJsonPostResponse (final String endpoint ,
921918 final byte [] body ,
922919 final Localization localization )
@@ -931,46 +928,28 @@ public static JsonObject getJsonPostResponse(final String endpoint,
931928 return JsonUtils .toJsonObject (getValidJsonResponseBody (response ));
932929 }
933930
934- public static JsonObject getJsonMobilePostResponse ( final String endpoint ,
935- final byte [] body ,
936- @ Nonnull final ContentCountry
937- contentCountry ,
938- final Localization localization )
939- throws IOException , ExtractionException {
931+ public static JsonObject getJsonAndroidPostResponse (
932+ final String endpoint ,
933+ final byte [] body ,
934+ @ Nonnull final ContentCountry contentCountry ,
935+ final Localization localization ,
936+ @ Nullable final String endPartOfUrlRequest ) throws IOException , ExtractionException {
940937 final Map <String , List <String >> headers = new HashMap <>();
941938 headers .put ("Content-Type" , Collections .singletonList ("application/json" ));
942939 // Spoofing an Android 11 device with the hardcoded version of the Android app
943940 headers .put ("User-Agent" , Collections .singletonList ("com.google.android.youtube/"
944- + MOBILE_YOUTUBE_CLIENT_VERSION + "Linux; U; Android 11; "
941+ + MOBILE_YOUTUBE_CLIENT_VERSION + " ( Linux; U; Android 11; "
945942 + contentCountry .getCountryCode () + ") gzip" ));
946943 headers .put ("x-goog-api-format-version" , Collections .singletonList ("2" ));
947944
948- final Response response = getDownloader ().post (
949- "https://youtubei.googleapis.com/youtubei/v1/" + endpoint + "?key="
950- + MOBILE_YOUTUBE_KEY , headers , body , localization );
951-
952- return JsonUtils .toJsonObject (getValidJsonResponseBody (response ));
953- }
954-
955- public static JsonArray getJsonResponse (final String url , final Localization localization )
956- throws IOException , ExtractionException {
957- final Map <String , List <String >> headers = new HashMap <>();
958- addYouTubeHeaders (headers );
959-
960- final Response response = getDownloader ().get (url , headers , localization );
961-
962- return JsonUtils .toJsonArray (getValidJsonResponseBody (response ));
963- }
945+ final String baseEndpointUrl = "https://youtubei.googleapis.com/youtubei/v1/" + endpoint
946+ + "?key=" + ANDROID_YOUTUBE_KEY ;
964947
965- public static JsonArray getJsonResponse (@ Nonnull final Page page ,
966- final Localization localization )
967- throws IOException , ExtractionException {
968- final Map <String , List <String >> headers = new HashMap <>();
969- addYouTubeHeaders (headers );
948+ final Response response = getDownloader ().post (isNullOrEmpty (endPartOfUrlRequest )
949+ ? baseEndpointUrl : baseEndpointUrl + endPartOfUrlRequest ,
950+ headers , body , localization );
970951
971- final Response response = getDownloader ().get (page .getUrl (), headers , localization );
972-
973- return JsonUtils .toJsonArray (getValidJsonResponseBody (response ));
952+ return JsonUtils .toJsonObject (getValidJsonResponseBody (response ));
974953 }
975954
976955 @ Nonnull
@@ -986,6 +965,13 @@ public static JsonBuilder<JsonObject> prepareDesktopJsonBuilder(
986965 .value ("gl" , contentCountry .getCountryCode ())
987966 .value ("clientName" , "WEB" )
988967 .value ("clientVersion" , getClientVersion ())
968+ .value ("originalUrl" , "https://www.youtube.com" )
969+ .value ("platform" , "DESKTOP" )
970+ .end ()
971+ .object ("request" )
972+ .array ("internalExperimentFlags" )
973+ .end ()
974+ .value ("useSsl" , true )
989975 .end ()
990976 .object ("user" )
991977 // TO DO: provide a way to enable restricted mode with:
@@ -1032,25 +1018,32 @@ public static JsonBuilder<JsonObject> prepareDesktopEmbedVideoJsonBuilder(
10321018 .value ("clientName" , "WEB" )
10331019 .value ("clientVersion" , getClientVersion ())
10341020 .value ("clientScreen" , "EMBED" )
1021+ .value ("originalUrl" , "https://www.youtube.com" )
1022+ .value ("platform" , "DESKTOP" )
10351023 .end ()
10361024 .object ("thirdParty" )
10371025 .value ("embedUrl" , "https://www.youtube.com/watch?v=" + videoId )
10381026 .end ()
1027+ .object ("request" )
1028+ .array ("internalExperimentFlags" )
1029+ .end ()
1030+ .value ("useSsl" , true )
1031+ .end ()
10391032 .object ("user" )
10401033 // TO DO: provide a way to enable restricted mode with:
10411034 // .value("enableSafetyMode", boolean)
10421035 .value ("lockedSafetyMode" , false )
10431036 .end ()
1044- .end ()
1045- .value ("videoId" , videoId );
1037+ .end ();
10461038 // @formatter:on
10471039 }
10481040
10491041 @ Nonnull
10501042 public static JsonBuilder <JsonObject > prepareAndroidMobileEmbedVideoJsonBuilder (
10511043 @ Nonnull final Localization localization ,
10521044 @ Nonnull final ContentCountry contentCountry ,
1053- @ Nonnull final String videoId ) {
1045+ @ Nonnull final String videoId ,
1046+ @ Nonnull final String contentPlaybackNonce ) {
10541047 // @formatter:off
10551048 return JsonObject .builder ()
10561049 .object ("context" )
@@ -1064,48 +1057,53 @@ public static JsonBuilder<JsonObject> prepareAndroidMobileEmbedVideoJsonBuilder(
10641057 .object ("thirdParty" )
10651058 .value ("embedUrl" , "https://www.youtube.com/watch?v=" + videoId )
10661059 .end ()
1060+ .object ("request" )
1061+ .array ("internalExperimentFlags" )
1062+ .end ()
1063+ .value ("useSsl" , true )
1064+ .end ()
10671065 .object ("user" )
10681066 // TO DO: provide a way to enable restricted mode with:
10691067 // .value("enableSafetyMode", boolean)
10701068 .value ("lockedSafetyMode" , false )
10711069 .end ()
10721070 .end ()
1073- .value ("videoId" , videoId );
1071+ .value (CPN , contentPlaybackNonce )
1072+ .value (VIDEO_ID , videoId );
10741073 // @formatter:on
10751074 }
10761075
10771076 @ Nonnull
1078- public static byte [] createPlayerBodyWithSts (final Localization localization ,
1079- final ContentCountry contentCountry ,
1080- final String videoId ,
1081- final boolean withThirdParty ,
1082- @ Nullable final String sts )
1083- throws IOException , ExtractionException {
1084- if (withThirdParty ) {
1085- // @formatter:off
1086- return JsonWriter .string (prepareDesktopEmbedVideoJsonBuilder (
1087- localization , contentCountry , videoId )
1088- .object ("playbackContext" )
1089- .object ("contentPlaybackContext" )
1090- .value ("signatureTimestamp" , sts )
1091- .end ()
1092- .end ()
1093- .done ())
1094- .getBytes (UTF_8 );
1095- // @formatter:on
1096- } else {
1097- // @formatter:off
1098- return JsonWriter .string (prepareDesktopJsonBuilder (localization , contentCountry )
1099- .value ("videoId" , videoId )
1100- .object ("playbackContext" )
1101- .object ("contentPlaybackContext" )
1102- .value ("signatureTimestamp" , sts )
1103- .end ()
1077+ public static byte [] createDesktopPlayerBody (
1078+ @ Nonnull final Localization localization ,
1079+ @ Nonnull final ContentCountry contentCountry ,
1080+ @ Nonnull final String videoId ,
1081+ @ Nonnull final String sts ,
1082+ final boolean isEmbedClientScreen ,
1083+ @ Nonnull final String contentPlaybackNonce ) throws IOException , ExtractionException {
1084+ // @formatter:off
1085+ return JsonWriter .string ((isEmbedClientScreen
1086+ ? prepareDesktopEmbedVideoJsonBuilder (localization , contentCountry ,
1087+ videoId )
1088+ : prepareDesktopJsonBuilder (localization , contentCountry ))
1089+ .object ("playbackContext" )
1090+ .object ("contentPlaybackContext" )
1091+ .value ("currentUrl" , "/watch?v=" + videoId )
1092+ .value ("vis" , 0 )
1093+ .value ("splay" , false )
1094+ .value ("autoCaptionsDefaultOn" , false )
1095+ .value ("autonavState" , "STATE_NONE" )
1096+ .value ("html5Preference" , "HTML5_PREF_WANTS" )
1097+ .value ("signatureTimestamp" , sts )
1098+ .value ("referer" , "https://www.youtube.com/watch?v=" + videoId )
1099+ .value ("lactMilliseconds" , "-1" )
11041100 .end ()
1105- .done ())
1106- .getBytes (UTF_8 );
1107- // @formatter:on
1108- }
1101+ .end ()
1102+ .value (CPN , contentPlaybackNonce )
1103+ .value (VIDEO_ID , videoId )
1104+ .done ())
1105+ .getBytes (StandardCharsets .UTF_8 );
1106+ // @formatter:on
11091107 }
11101108
11111109 /**
@@ -1381,4 +1379,47 @@ public static String unescapeDocument(@Nonnull final String doc) {
13811379 .replaceAll ("\\ \\ x5b" , "[" )
13821380 .replaceAll ("\\ \\ x5d" , "]" );
13831381 }
1382+
1383+ /**
1384+ * Generate a content playback nonce (also called {@code cpn}), sent by YouTube clients in
1385+ * playback requests (and also for some clients, in the player request body).
1386+ *
1387+ * @return a content playback nonce string
1388+ */
1389+ @ Nonnull
1390+ public static String generateContentPlaybackNonce () {
1391+ final SecureRandom random = new SecureRandom ();
1392+ final StringBuilder stringBuilder = new StringBuilder ();
1393+
1394+ for (int i = 0 ; i < 16 ; i ++) {
1395+ stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1396+ (random .nextInt (128 ) + 1 ) & 63 ));
1397+ }
1398+
1399+ return stringBuilder .toString ();
1400+ }
1401+
1402+ /**
1403+ * Try to generate a {@code t} parameter, sent by mobile clients as a query of the player
1404+ * request.
1405+ *
1406+ * <p>
1407+ * Some researches needs to be done to know how this parameter, unique at each request, is
1408+ * generated.
1409+ * </p>
1410+ *
1411+ * @return a 12 characters string to try to reproduce the {@code} parameter
1412+ */
1413+ @ Nonnull
1414+ public static String generateTParameter () {
1415+ final SecureRandom random = new SecureRandom ();
1416+ final StringBuilder stringBuilder = new StringBuilder ();
1417+
1418+ for (int i = 0 ; i < 12 ; i ++) {
1419+ stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1420+ (random .nextInt (128 ) + 1 ) & 63 ));
1421+ }
1422+
1423+ return stringBuilder .toString ();
1424+ }
13841425}
0 commit comments