77import static org .schabi .newpipe .extractor .utils .Utils .UTF_8 ;
88import static org .schabi .newpipe .extractor .utils .Utils .getStringResultFromRegexArray ;
99import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
10+ import static org .schabi .newpipe .extractor .utils .Utils .randomStringFromAlphabet ;
1011
1112import com .grack .nanojson .JsonArray ;
1213import com .grack .nanojson .JsonBuilder ;
3637import java .net .URL ;
3738import java .net .URLDecoder ;
3839import java .nio .charset .StandardCharsets ;
39- import java .security .SecureRandom ;
4040import java .time .LocalDate ;
4141import java .time .OffsetDateTime ;
4242import java .time .ZoneOffset ;
@@ -83,6 +83,11 @@ private YoutubeParsingHelper() {
8383 public static final String CPN = "cpn" ;
8484 public static final String VIDEO_ID = "videoId" ;
8585
86+ /**
87+ * Seed that will be used for video tests, in order to mock video requests.
88+ */
89+ private static final long SEED_FOR_VIDEOS_TESTS = 3000 ;
90+
8691 private static final String HARDCODED_CLIENT_VERSION = "2.20220114.01.00" ;
8792 private static final String HARDCODED_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" ;
8893
@@ -100,13 +105,17 @@ private YoutubeParsingHelper() {
100105 private static boolean keyAndVersionExtracted = false ;
101106 @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
102107 private static Optional <Boolean > hardcodedClientVersionAndKeyValid = Optional .empty ();
108+
103109 private static final String [] INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES =
104110 {"INNERTUBE_CONTEXT_CLIENT_VERSION\" :\" ([0-9\\ .]+?)\" " ,
105111 "innertube_context_client_version\" :\" ([0-9\\ .]+?)\" " ,
106112 "client.version=([0-9\\ .]+)" };
107113 private static final String [] INNERTUBE_API_KEY_REGEXES =
108114 {"INNERTUBE_API_KEY\" :\" ([0-9a-zA-Z_-]+?)\" " ,
109115 "innertubeApiKey\" :\" ([0-9a-zA-Z_-]+?)\" " };
116+ private static final String [] INITIAL_DATA_REGEXES =
117+ {"window\\ [\" ytInitialData\" \\ ]\\ s*=\\ s*(\\ {.*?\\ });" ,
118+ "var\\ s*ytInitialData\\ s*=\\ s*(\\ {.*?\\ });" };
110119 private static final String INNERTUBE_CLIENT_NAME_REGEX =
111120 "INNERTUBE_CONTEXT_CLIENT_NAME\" :([0-9]+?)," ;
112121
@@ -116,13 +125,24 @@ private YoutubeParsingHelper() {
116125 private static Random numberGenerator = new Random ();
117126
118127 /**
119- * <code>PENDING+</code> means that the user did not yet submit their choices.
128+ * {@code PENDING+} means that the user did not yet submit their choices.
129+ *
130+ * <p>
120131 * Therefore, YouTube & Google should not track the user, because they did not give consent.
132+ * </p>
133+ *
134+ * <p>
121135 * The three digits at the end can be random, but are required.
136+ * </p>
122137 */
123138 private static final String CONSENT_COOKIE_VALUE = "PENDING+" ;
139+
124140 /**
125- * Youtube <code>CONSENT</code> cookie. Should prevent redirect to consent.youtube.com
141+ * YouTube {@code CONSENT} cookie.
142+ *
143+ * <p>
144+ * Should prevent redirect to {@code consent.youtube.com}.
145+ * </p>
126146 */
127147 private static final String CONSENT_COOKIE = "CONSENT=" + CONSENT_COOKIE_VALUE ;
128148
@@ -439,17 +459,10 @@ public static PlaylistInfo.PlaylistType extractPlaylistTypeFromPlaylistUrl(
439459 }
440460 }
441461
442- public static JsonObject getInitialData (final String html ) throws ParsingException {
462+ private static JsonObject getInitialData (final String html ) throws ParsingException {
443463 try {
444- try {
445- final String initialData = Parser .matchGroup1 (
446- "window\\ [\" ytInitialData\" \\ ]\\ s*=\\ s*(\\ {.*?\\ });" , html );
447- return JsonParser .object ().from (initialData );
448- } catch (final Parser .RegexException e ) {
449- final String initialData = Parser .matchGroup1 (
450- "var\\ s*ytInitialData\\ s*=\\ s*(\\ {.*?\\ });" , html );
451- return JsonParser .object ().from (initialData );
452- }
464+ return JsonParser .object ().from (getStringResultFromRegexArray (html ,
465+ INITIAL_DATA_REGEXES , 1 ));
453466 } catch (final JsonParserException | Parser .RegexException e ) {
454467 throw new ParsingException ("Could not get ytInitialData" , e );
455468 }
@@ -572,7 +585,7 @@ private static void extractClientVersionAndKeyFromHtmlSearchResultsPage()
572585 key = getStringResultFromRegexArray (html , INNERTUBE_API_KEY_REGEXES , 1 );
573586 } catch (final Parser .RegexException e ) {
574587 throw new ParsingException (
575- "Could not extract YouTube WEB InnerTube client version and API key from HTML search results page" );
588+ "Could not extract YouTube WEB InnerTube client version and API key from HTML search results page" , e );
576589 }
577590 keyAndVersionExtracted = true ;
578591 }
@@ -730,8 +743,7 @@ public static String[] getYoutubeMusicKey()
730743 final String response = getDownloader ().get (url , headers ).responseBody ();
731744 musicClientVersion = getStringResultFromRegexArray (response ,
732745 INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES , 1 );
733- musicKey = getStringResultFromRegexArray (response ,
734- INNERTUBE_API_KEY_REGEXES , 1 );
746+ musicKey = getStringResultFromRegexArray (response , INNERTUBE_API_KEY_REGEXES , 1 );
735747 musicClientName = Parser .matchGroup1 (INNERTUBE_CLIENT_NAME_REGEX , response );
736748 } catch (final Exception e ) {
737749 final String url = "https://music.youtube.com/" ;
@@ -815,10 +827,11 @@ public static String getUrlFromNavigationEndpoint(@Nonnull final JsonObject navi
815827 }
816828
817829 /**
818- * Get the text from a JSON object that has either a simpleText or a runs array.
830+ * Get the text from a JSON object that has either a {@code simpleText} or a {@code runs}
831+ * array.
819832 *
820833 * @param textObject JSON object to get the text from
821- * @param html whether to return HTML, by parsing the navigationEndpoint
834+ * @param html whether to return HTML, by parsing the {@code navigationEndpoint}
822835 * @return text in the JSON object or {@code null}
823836 */
824837 @ Nullable
@@ -1495,15 +1508,7 @@ public static String unescapeDocument(@Nonnull final String doc) {
14951508 */
14961509 @ Nonnull
14971510 public static String generateContentPlaybackNonce () {
1498- final SecureRandom random = new SecureRandom ();
1499- final StringBuilder stringBuilder = new StringBuilder ();
1500-
1501- for (int i = 0 ; i < 16 ; i ++) {
1502- stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1503- (random .nextInt (128 ) + 1 ) & 63 ));
1504- }
1505-
1506- return stringBuilder .toString ();
1511+ return randomStringFromAlphabet (CONTENT_PLAYBACK_NONCE_ALPHABET , 16 );
15071512 }
15081513
15091514 /**
@@ -1519,14 +1524,23 @@ public static String generateContentPlaybackNonce() {
15191524 */
15201525 @ Nonnull
15211526 public static String generateTParameter () {
1522- final SecureRandom random = new SecureRandom ();
1523- final StringBuilder stringBuilder = new StringBuilder ();
1524-
1525- for (int i = 0 ; i < 12 ; i ++) {
1526- stringBuilder .append (CONTENT_PLAYBACK_NONCE_ALPHABET .charAt (
1527- (random .nextInt (128 ) + 1 ) & 63 ));
1528- }
1527+ return randomStringFromAlphabet (CONTENT_PLAYBACK_NONCE_ALPHABET , 12 );
1528+ }
15291529
1530- return stringBuilder .toString ();
1530+ /**
1531+ * Set the seed for video tests.
1532+ *
1533+ * <p>
1534+ * This seed will be used to generate the same {@code t} and {@code cpn} values between
1535+ * different execution of tests so mocks can be used for stream tests.
1536+ * </p>
1537+ *
1538+ * <p>
1539+ * This method will call {@link Utils#setSecureRandomSeed(long)} with the
1540+ * {@link #SEED_FOR_VIDEOS_TESTS value}.
1541+ * </p>
1542+ */
1543+ public static void setSeedForVideoTests () {
1544+ Utils .setSecureRandomSeed (SEED_FOR_VIDEOS_TESTS );
15311545 }
15321546}
0 commit comments