28
28
import java .time .OffsetDateTime ;
29
29
import java .time .ZoneOffset ;
30
30
import java .time .format .DateTimeParseException ;
31
- import java .util .ArrayList ;
32
- import java .util .Collections ;
33
- import java .util .HashMap ;
34
- import java .util .List ;
35
- import java .util .Map ;
36
- import java .util .Objects ;
31
+ import java .util .*;
37
32
38
33
import javax .annotation .Nonnull ;
39
34
import javax .annotation .Nullable ;
@@ -79,6 +74,19 @@ private YoutubeParsingHelper() {
79
74
private static final String [] HARDCODED_YOUTUBE_MUSIC_KEYS = {"AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30" , "67" , "0.1" };
80
75
private static String [] youtubeMusicKeys ;
81
76
77
+ private static Random numberGenerator = new Random ();
78
+
79
+ /**
80
+ * <code>PENDING+</code> means that the user did not yet submit their choices.
81
+ * Therefore, YouTube & Google should not track the user, because they did not give consent.
82
+ * The three digits at the end can be random, but are required.
83
+ */
84
+ private static final String CONSENT_COOKIE_VALUE = "PENDING+" ;
85
+ /**
86
+ * Youtube <code>CONSENT</code> cookie. Should prevent redirect to consent.youtube.com
87
+ */
88
+ private static final String CONSENT_COOKIE = "CONSENT=" + CONSENT_COOKIE_VALUE ;
89
+
82
90
private static final String FEED_BASE_CHANNEL_ID = "https://www.youtube.com/feeds/videos.xml?channel_id=" ;
83
91
private static final String FEED_BASE_USER = "https://www.youtube.com/feeds/videos.xml?user=" ;
84
92
@@ -388,6 +396,15 @@ public static void resetClientVersionAndKey() {
388
396
key = null ;
389
397
}
390
398
399
+ /**
400
+ * <p>
401
+ * <b>Only use in tests.</b>
402
+ * </p>
403
+ */
404
+ public static void setNumberGenerator (Random random ) {
405
+ numberGenerator = random ;
406
+ }
407
+
391
408
public static boolean areHardcodedYoutubeMusicKeysValid () throws IOException , ReCaptchaException {
392
409
final String url = "https://music.youtube.com/youtubei/v1/search?alt=json&key=" + HARDCODED_YOUTUBE_MUSIC_KEYS [0 ];
393
410
@@ -427,6 +444,7 @@ public static boolean areHardcodedYoutubeMusicKeysValid() throws IOException, Re
427
444
headers .put ("Origin" , Collections .singletonList ("https://music.youtube.com" ));
428
445
headers .put ("Referer" , Collections .singletonList ("music.youtube.com" ));
429
446
headers .put ("Content-Type" , Collections .singletonList ("application/json" ));
447
+ addCookieHeader (headers );
430
448
431
449
final String response = getDownloader ().post (url , headers , json ).responseBody ();
432
450
@@ -629,34 +647,19 @@ public static String getValidJsonResponseBody(final Response response)
629
647
public static Response getResponse (final String url , final Localization localization )
630
648
throws IOException , ExtractionException {
631
649
final Map <String , List <String >> headers = new HashMap <>();
632
- headers .put ("X-YouTube-Client-Name" , Collections .singletonList ("1" ));
633
- headers .put ("X-YouTube-Client-Version" , Collections .singletonList (getClientVersion ()));
650
+ addYouTubeHeaders (headers );
634
651
635
652
final Response response = getDownloader ().get (url , headers , localization );
636
653
getValidJsonResponseBody (response );
637
654
638
655
return response ;
639
656
}
640
657
641
- public static String extractCookieValue (final String cookieName , final Response response ) {
642
- final List <String > cookies = response .responseHeaders ().get ("set-cookie" );
643
- int startIndex ;
644
- String result = "" ;
645
- for (final String cookie : cookies ) {
646
- startIndex = cookie .indexOf (cookieName );
647
- if (startIndex != -1 ) {
648
- result = cookie .substring (startIndex + cookieName .length () + "=" .length (),
649
- cookie .indexOf (";" , startIndex ));
650
- }
651
- }
652
- return result ;
653
- }
654
-
655
658
public static JsonArray getJsonResponse (final String url , final Localization localization )
656
659
throws IOException , ExtractionException {
657
660
Map <String , List <String >> headers = new HashMap <>();
658
- headers . put ( "X-YouTube-Client-Name" , Collections . singletonList ( "1" ) );
659
- headers . put ( "X-YouTube-Client-Version" , Collections . singletonList ( getClientVersion ()));
661
+ addYouTubeHeaders ( headers );
662
+
660
663
final Response response = getDownloader ().get (url , headers , localization );
661
664
662
665
return JsonUtils .toJsonArray (getValidJsonResponseBody (response ));
@@ -665,17 +668,69 @@ public static JsonArray getJsonResponse(final String url, final Localization loc
665
668
public static JsonArray getJsonResponse (final Page page , final Localization localization )
666
669
throws IOException , ExtractionException {
667
670
final Map <String , List <String >> headers = new HashMap <>();
668
- if (!isNullOrEmpty (page .getCookies ())) {
669
- headers .put ("Cookie" , Collections .singletonList (join (";" , "=" , page .getCookies ())));
670
- }
671
- headers .put ("X-YouTube-Client-Name" , Collections .singletonList ("1" ));
672
- headers .put ("X-YouTube-Client-Version" , Collections .singletonList (getClientVersion ()));
671
+ addYouTubeHeaders (headers );
673
672
674
673
final Response response = getDownloader ().get (page .getUrl (), headers , localization );
675
674
676
675
return JsonUtils .toJsonArray (getValidJsonResponseBody (response ));
677
676
}
678
677
678
+ /**
679
+ * Add required headers and cookies to an existing headers Map.
680
+ * @see #addClientInfoHeaders(Map)
681
+ * @see #addCookieHeader(Map)
682
+ */
683
+ public static void addYouTubeHeaders (final Map <String , List <String >> headers )
684
+ throws IOException , ExtractionException {
685
+ addClientInfoHeaders (headers );
686
+ addCookieHeader (headers );
687
+ }
688
+
689
+ /**
690
+ * Add the <code>X-YouTube-Client-Name</code> and <code>X-YouTube-Client-Version</code> headers.
691
+ * @param headers The headers which should be completed
692
+ */
693
+ public static void addClientInfoHeaders (final Map <String , List <String >> headers )
694
+ throws IOException , ExtractionException {
695
+ if (headers .get ("X-YouTube-Client-Name" ) == null ) {
696
+ headers .put ("X-YouTube-Client-Name" , Collections .singletonList ("1" ));
697
+ }
698
+ if (headers .get ("X-YouTube-Client-Version" ) == null ) {
699
+ headers .put ("X-YouTube-Client-Version" , Collections .singletonList (getClientVersion ()));
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Add the <code>CONSENT</code> cookie to prevent redirect to <code>consent.youtube.com</code>
705
+ * @see #CONSENT_COOKIE
706
+ * @param headers the headers which should be completed
707
+ */
708
+ public static void addCookieHeader (final Map <String , List <String >> headers ) {
709
+ if (headers .get ("Cookie" ) == null ) {
710
+ headers .put ("Cookie" , Arrays .asList (generateConsentCookie ()));
711
+ } else {
712
+ headers .get ("Cookie" ).add (generateConsentCookie ());
713
+ }
714
+ }
715
+
716
+ public static String generateConsentCookie () {
717
+ return CONSENT_COOKIE + 100 + numberGenerator .nextInt (900 );
718
+ }
719
+
720
+ public static String extractCookieValue (final String cookieName , final Response response ) {
721
+ final List <String > cookies = response .responseHeaders ().get ("set-cookie" );
722
+ int startIndex ;
723
+ String result = "" ;
724
+ for (final String cookie : cookies ) {
725
+ startIndex = cookie .indexOf (cookieName );
726
+ if (startIndex != -1 ) {
727
+ result = cookie .substring (startIndex + cookieName .length () + "=" .length (),
728
+ cookie .indexOf (";" , startIndex ));
729
+ }
730
+ }
731
+ return result ;
732
+ }
733
+
679
734
/**
680
735
* Shared alert detection function, multiple endpoints return the error similarly structured.
681
736
* <p>
0 commit comments