From 9251be8462d3fcac63635e341b6f3a11881938b3 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Wed, 31 Mar 2021 15:59:57 +0200 Subject: [PATCH] added ability to create consent cookie --- youtube_transcript_api/__init__.py | 3 +- youtube_transcript_api/_api.py | 7 +- youtube_transcript_api/_errors.py | 17 +- youtube_transcript_api/_transcripts.py | 18 +- .../assets/youtube_consent_page.html.static | 160 ++++++++++++++++++ .../youtube_consent_page_invalid.html.static | 160 ++++++++++++++++++ youtube_transcript_api/test/test_api.py | 41 ++++- 7 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 youtube_transcript_api/test/assets/youtube_consent_page.html.static create mode 100644 youtube_transcript_api/test/assets/youtube_consent_page_invalid.html.static diff --git a/youtube_transcript_api/__init__.py b/youtube_transcript_api/__init__.py index baefd02..2d58d34 100644 --- a/youtube_transcript_api/__init__.py +++ b/youtube_transcript_api/__init__.py @@ -10,5 +10,6 @@ from ._errors import ( TranslationLanguageNotAvailable, NoTranscriptAvailable, CookiePathInvalid, - CookiesInvalid + CookiesInvalid, + FailedToCreateConsentCookie, ) diff --git a/youtube_transcript_api/_api.py b/youtube_transcript_api/_api.py index ad69b90..37bd6b2 100644 --- a/youtube_transcript_api/_api.py +++ b/youtube_transcript_api/_api.py @@ -129,12 +129,11 @@ class YouTubeTranscriptApi(object): @classmethod def _load_cookies(cls, cookies, video_id): - cookie_jar = {} try: cookie_jar = cookiejar.MozillaCookieJar() cookie_jar.load(cookies) + if not cookie_jar: + raise CookiesInvalid(video_id) + return cookie_jar except CookieLoadError: raise CookiePathInvalid(video_id) - if not cookie_jar: - raise CookiesInvalid(video_id) - return cookie_jar diff --git a/youtube_transcript_api/_errors.py b/youtube_transcript_api/_errors.py index c3afb32..cd645b5 100644 --- a/youtube_transcript_api/_errors.py +++ b/youtube_transcript_api/_errors.py @@ -40,10 +40,15 @@ class VideoUnavailable(CouldNotRetrieveTranscript): class TooManyRequests(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = ("YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. One of the following things can be done to work around this:\n\ - - Manually solve the captcha in a browser and export the cookie. Read here how to use that cookie with youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\ - - Use a different IP address\n\ - - Wait until the ban on your IP has been lifted") + CAUSE_MESSAGE = ( + 'YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. ' + 'One of the following things can be done to work around this:\n\ + - Manually solve the captcha in a browser and export the cookie. ' + 'Read here how to use that cookie with ' + 'youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\ + - Use a different IP address\n\ + - Wait until the ban on your IP has been lifted' + ) class TranscriptsDisabled(CouldNotRetrieveTranscript): @@ -70,6 +75,10 @@ class CookiesInvalid(CouldNotRetrieveTranscript): CAUSE_MESSAGE = 'The cookies provided are not valid (may have expired)' +class FailedToCreateConsentCookie(CouldNotRetrieveTranscript): + CAUSE_MESSAGE = 'Failed to automatically give consent to saving cookies' + + class NoTranscriptFound(CouldNotRetrieveTranscript): CAUSE_MESSAGE = ( 'No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n' diff --git a/youtube_transcript_api/_transcripts.py b/youtube_transcript_api/_transcripts.py index 10c5ec0..b0d6f38 100644 --- a/youtube_transcript_api/_transcripts.py +++ b/youtube_transcript_api/_transcripts.py @@ -20,6 +20,7 @@ from ._errors import ( NotTranslatable, TranslationLanguageNotAvailable, NoTranscriptAvailable, + FailedToCreateConsentCookie, ) from ._settings import WATCH_URL @@ -32,7 +33,7 @@ class TranscriptListFetcher(object): return TranscriptList.build( self._http_client, video_id, - self._extract_captions_json(self._fetch_html(video_id), video_id) + self._extract_captions_json(self._fetch_video_html(video_id), video_id) ) def _extract_captions_json(self, html, video_id): @@ -55,6 +56,21 @@ class TranscriptListFetcher(object): return captions_json + def _create_consent_cookie(self, html, video_id): + match = re.search('name="v" value="(.*?)"', html) + if match is None: + raise FailedToCreateConsentCookie(video_id) + self._http_client.cookies.set('CONSENT', 'YES+' + match.group(1), domain='.youtube.com') + + def _fetch_video_html(self, video_id): + html = self._fetch_html(video_id) + if 'action="https://consent.youtube.com/s"' in html: + self._create_consent_cookie(html, video_id) + html = self._fetch_html(video_id) + if 'action="https://consent.youtube.com/s"' in html: + raise FailedToCreateConsentCookie(video_id) + return html + def _fetch_html(self, video_id): return self._http_client.get(WATCH_URL.format(video_id=video_id)).text.replace( '\\u0026', '&' diff --git a/youtube_transcript_api/test/assets/youtube_consent_page.html.static b/youtube_transcript_api/test/assets/youtube_consent_page.html.static new file mode 100644 index 0000000..40b4235 --- /dev/null +++ b/youtube_transcript_api/test/assets/youtube_consent_page.html.static @@ -0,0 +1,160 @@ +Bevor Sie zu YouTube weitergehen
Anmelden
YouTube ein Google-Unternehmen

Bevor Sie zu YouTube weitergehen

Google verwendet Cookies und Daten, um Dienste und Werbung zur Verfügung zu stellen, zu verwalten und zu verbessern. Wenn Sie zustimmen, nutzen wir Cookies für diese Zwecke und dazu, Inhalte und Werbung für Sie zu personalisieren, damit Sie z. B. relevantere Google-Suchergebnisse und relevantere Werbung bei YouTube erhalten. Die Personalisierung erfolgt auf Grundlage Ihrer Aktivitäten, beispielsweise Ihrer Google-Suchanfragen und der Videos, die Sie sich bei YouTube ansehen. Wir verwenden diese Daten auch für Analysen und Messungen. Klicken Sie auf „Anpassen“, um sich weitere Optionen anzusehen, oder besuchen Sie g.co/privacytools. Darüber hinaus haben Sie die Möglichkeit, Ihre Browsereinstellungen so zu konfigurieren, dass einige oder alle Cookies blockiert werden.

Anpassen
\ No newline at end of file diff --git a/youtube_transcript_api/test/assets/youtube_consent_page_invalid.html.static b/youtube_transcript_api/test/assets/youtube_consent_page_invalid.html.static new file mode 100644 index 0000000..db74c38 --- /dev/null +++ b/youtube_transcript_api/test/assets/youtube_consent_page_invalid.html.static @@ -0,0 +1,160 @@ +Bevor Sie zu YouTube weitergehen
Anmelden
YouTube ein Google-Unternehmen

Bevor Sie zu YouTube weitergehen

Google verwendet Cookies und Daten, um Dienste und Werbung zur Verfügung zu stellen, zu verwalten und zu verbessern. Wenn Sie zustimmen, nutzen wir Cookies für diese Zwecke und dazu, Inhalte und Werbung für Sie zu personalisieren, damit Sie z. B. relevantere Google-Suchergebnisse und relevantere Werbung bei YouTube erhalten. Die Personalisierung erfolgt auf Grundlage Ihrer Aktivitäten, beispielsweise Ihrer Google-Suchanfragen und der Videos, die Sie sich bei YouTube ansehen. Wir verwenden diese Daten auch für Analysen und Messungen. Klicken Sie auf „Anpassen“, um sich weitere Optionen anzusehen, oder besuchen Sie g.co/privacytools. Darüber hinaus haben Sie die Möglichkeit, Ihre Browsereinstellungen so zu konfigurieren, dass einige oder alle Cookies blockiert werden.

Anpassen
\ No newline at end of file diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index 7650cf4..240164d 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -17,7 +17,8 @@ from youtube_transcript_api import ( NotTranslatable, TranslationLanguageNotAvailable, CookiePathInvalid, - CookiesInvalid + CookiesInvalid, + FailedToCreateConsentCookie, ) @@ -44,6 +45,7 @@ class TestYouTubeTranscriptApi(TestCase): ) def tearDown(self): + httpretty.reset() httpretty.disable() def test_get_transcript(self): @@ -125,6 +127,43 @@ class TestYouTubeTranscriptApi(TestCase): self.assertEqual(len(query_string['lang']), 1) self.assertEqual(query_string['lang'][0], 'en') + def test_get_transcript__create_consent_cookie_if_needed(self): + httpretty.register_uri( + httpretty.GET, + 'https://www.youtube.com/watch', + body=load_asset('youtube_consent_page.html.static') + ) + + YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + self.assertEqual(len(httpretty.latest_requests()), 3) + for request in httpretty.latest_requests()[1:]: + self.assertEqual(request.headers['cookie'], 'CONSENT=YES+cb.20210328-17-p0.de+FX+119') + + def test_get_transcript__exception_if_create_consent_cookie_failed(self): + httpretty.register_uri( + httpretty.GET, + 'https://www.youtube.com/watch', + body=load_asset('youtube_consent_page.html.static') + ) + httpretty.register_uri( + httpretty.GET, + 'https://www.youtube.com/watch', + body=load_asset('youtube_consent_page.html.static') + ) + + with self.assertRaises(FailedToCreateConsentCookie): + YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + + def test_get_transcript__exception_if_consent_cookie_age_invalid(self): + httpretty.register_uri( + httpretty.GET, + 'https://www.youtube.com/watch', + body=load_asset('youtube_consent_page_invalid.html.static') + ) + + with self.assertRaises(FailedToCreateConsentCookie): + YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + def test_get_transcript__exception_if_video_unavailable(self): httpretty.register_uri( httpretty.GET,