2323import java .net .URL ;
2424import java .net .URLDecoder ;
2525import java .nio .charset .StandardCharsets ;
26- import java .security .SecureRandom ;
2726import java .time .LocalDate ;
2827import java .time .OffsetDateTime ;
2928import java .time .ZoneOffset ;
@@ -65,6 +64,11 @@ private YoutubeParsingHelper() {
6564 public static final String CPN = "cpn" ;
6665 public static final String VIDEO_ID = "videoId" ;
6766
67+ /**
68+ * Seed that will be used for video tests, in order to mock video requests.
69+ */
70+ private static final long SEED_FOR_VIDEOS_TESTS = 3000 ;
71+
6872 private static final String HARDCODED_CLIENT_VERSION = "2.20220114.01.00" ;
6973 private static final String HARDCODED_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" ;
7074
@@ -82,13 +86,17 @@ private YoutubeParsingHelper() {
8286 private static boolean keyAndVersionExtracted = false ;
8387 @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
8488 private static Optional <Boolean > hardcodedClientVersionAndKeyValid = Optional .empty ();
89+
8590 private static final String [] INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES =
8691 {"INNERTUBE_CONTEXT_CLIENT_VERSION\" :\" ([0-9\\ .]+?)\" " ,
8792 "innertube_context_client_version\" :\" ([0-9\\ .]+?)\" " ,
8893 "client.version=([0-9\\ .]+)" };
8994 private static final String [] INNERTUBE_API_KEY_REGEXES =
9095 {"INNERTUBE_API_KEY\" :\" ([0-9a-zA-Z_-]+?)\" " ,
9196 "innertubeApiKey\" :\" ([0-9a-zA-Z_-]+?)\" " };
97+ private static final String [] INITIAL_DATA_REGEXES =
98+ {"window\\ [\" ytInitialData\" \\ ]\\ s*=\\ s*(\\ {.*?\\ });" ,
99+ "var\\ s*ytInitialData\\ s*=\\ s*(\\ {.*?\\ });" };
92100 private static final String INNERTUBE_CLIENT_NAME_REGEX =
93101 "INNERTUBE_CONTEXT_CLIENT_NAME\" :([0-9]+?)," ;
94102
@@ -98,13 +106,24 @@ private YoutubeParsingHelper() {
98106 private static Random numberGenerator = new Random ();
99107
100108 /**
101- * <code>PENDING+</code> means that the user did not yet submit their choices.
109+ * {@code PENDING+} means that the user did not yet submit their choices.
110+ *
111+ * <p>
102112 * Therefore, YouTube & Google should not track the user, because they did not give consent.
113+ * </p>
114+ *
115+ * <p>
103116 * The three digits at the end can be random, but are required.
117+ * </p>
104118 */
105119 private static final String CONSENT_COOKIE_VALUE = "PENDING+" ;
120+
106121 /**
107- * Youtube <code>CONSENT</code> cookie. Should prevent redirect to consent.youtube.com
122+ * YouTube {@code CONSENT} cookie.
123+ *
124+ * <p>
125+ * Should prevent redirect to {@code consent.youtube.com}.
126+ * </p>
108127 */
109128 private static final String CONSENT_COOKIE = "CONSENT=" + CONSENT_COOKIE_VALUE ;
110129
@@ -312,17 +331,10 @@ public static String extractVideoIdFromMixId(@Nonnull final String playlistId)
312331 }
313332 }
314333
315- public static JsonObject getInitialData (final String html ) throws ParsingException {
334+ private static JsonObject getInitialData (final String html ) throws ParsingException {
316335 try {
317- try {
318- final String initialData = Parser .matchGroup1 (
319- "window\\ [\" ytInitialData\" \\ ]\\ s*=\\ s*(\\ {.*?\\ });" , html );
320- return JsonParser .object ().from (initialData );
321- } catch (final Parser .RegexException e ) {
322- final String initialData = Parser .matchGroup1 (
323- "var\\ s*ytInitialData\\ s*=\\ s*(\\ {.*?\\ });" , html );
324- return JsonParser .object ().from (initialData );
325- }
336+ return JsonParser .object ().from (getStringResultFromRegexArray (html ,
337+ INITIAL_DATA_REGEXES , 1 ));
326338 } catch (final JsonParserException | Parser .RegexException e ) {
327339 throw new ParsingException ("Could not get ytInitialData" , e );
328340 }
@@ -445,7 +457,7 @@ private static void extractClientVersionAndKeyFromHtmlSearchResultsPage()
445457 key = getStringResultFromRegexArray (html , INNERTUBE_API_KEY_REGEXES , 1 );
446458 } catch (final Parser .RegexException e ) {
447459 throw new ParsingException (
448- "Could not extract YouTube WEB InnerTube client version and API key from HTML search results page" );
460+ "Could not extract YouTube WEB InnerTube client version and API key from HTML search results page" , e );
449461 }
450462 keyAndVersionExtracted = true ;
451463 }
@@ -609,8 +621,7 @@ public static String[] getYoutubeMusicKey()
609621 final String response = getDownloader ().get (url , headers ).responseBody ();
610622 musicClientVersion = getStringResultFromRegexArray (response ,
611623 INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES , 1 );
612- musicKey = getStringResultFromRegexArray (response ,
613- INNERTUBE_API_KEY_REGEXES , 1 );
624+ musicKey = getStringResultFromRegexArray (response , INNERTUBE_API_KEY_REGEXES , 1 );
614625 musicClientName = Parser .matchGroup1 (INNERTUBE_CLIENT_NAME_REGEX , response );
615626 } catch (final Exception e ) {
616627 final String url = "https://music.youtube.com/" ;
@@ -696,10 +707,11 @@ public static String getUrlFromNavigationEndpoint(@Nonnull final JsonObject navi
696707 }
697708
698709 /**
699- * Get the text from a JSON object that has either a simpleText or a runs array.
710+ * Get the text from a JSON object that has either a {@code simpleText} or a {@code runs}
711+ * array.
700712 *
701713 * @param textObject JSON object to get the text from
702- * @param html whether to return HTML, by parsing the navigationEndpoint
714+ * @param html whether to return HTML, by parsing the {@code navigationEndpoint}
703715 * @return text in the JSON object or {@code null}
704716 */
705717 @ Nullable
@@ -1343,15 +1355,7 @@ public static String unescapeDocument(@Nonnull final String doc) {
13431355 */
13441356 @ Nonnull
13451357 public static String generateContentPlaybackNonce () {
1346- final SecureRandom random = new SecureRandom ();
1347- final StringBuilder stringBuilder = new StringBuilder ();
1348-
1349- for (int i = 0 ; i < 16 ; i ++) {
1350- stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1351- (random .nextInt (128 ) + 1 ) & 63 ));
1352- }
1353-
1354- return stringBuilder .toString ();
1358+ return randomStringFromAlphabet (CONTENT_PLAYBACK_NONCE_ALPHABET , 16 );
13551359 }
13561360
13571361 /**
@@ -1367,14 +1371,23 @@ public static String generateContentPlaybackNonce() {
13671371 */
13681372 @ Nonnull
13691373 public static String generateTParameter () {
1370- final SecureRandom random = new SecureRandom ();
1371- final StringBuilder stringBuilder = new StringBuilder ();
1372-
1373- for (int i = 0 ; i < 12 ; i ++) {
1374- stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1375- (random .nextInt (128 ) + 1 ) & 63 ));
1376- }
1374+ return randomStringFromAlphabet (CONTENT_PLAYBACK_NONCE_ALPHABET , 12 );
1375+ }
13771376
1378- return stringBuilder .toString ();
1377+ /**
1378+ * Set the seed for video tests.
1379+ *
1380+ * <p>
1381+ * This seed will be used to generate the same {@code t} and {@code cpn} values between
1382+ * different execution of tests so mocks can be used for stream tests.
1383+ * </p>
1384+ *
1385+ * <p>
1386+ * This method will call {@link Utils#setSecureRandomSeed(long)} with the
1387+ * {@link #SEED_FOR_VIDEOS_TESTS value}.
1388+ * </p>
1389+ */
1390+ public static void setSeedForVideoTests () {
1391+ Utils .setSecureRandomSeed (SEED_FOR_VIDEOS_TESTS );
13791392 }
13801393}
0 commit comments