From 145bd631c52d9911cdfd8c40d5e09f0fa6b0b579 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Wed, 21 Jul 2021 14:42:43 +0200 Subject: [PATCH] [nebula] Authentication via tokens from cookie jar (#537) Closes #496 Co-authored-by: hheimbuerger, TpmKranz --- yt_dlp/extractor/nebula.py | 65 ++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/yt_dlp/extractor/nebula.py b/yt_dlp/extractor/nebula.py index 1a0a394f1a..4426a8fdc9 100644 --- a/yt_dlp/extractor/nebula.py +++ b/yt_dlp/extractor/nebula.py @@ -2,9 +2,11 @@ from __future__ import unicode_literals import json +import time +from urllib.error import HTTPError from .common import InfoExtractor -from ..compat import compat_str +from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote from ..utils import ( ExtractorError, parse_iso8601, @@ -78,7 +80,9 @@ class NebulaIE(InfoExtractor): ] _NETRC_MACHINE = 'watchnebula' - def _retrieve_nebula_auth(self, video_id): + _nebula_token = None + + def _retrieve_nebula_auth(self): """ Log in to Nebula, and returns a Nebula API token """ @@ -91,7 +95,7 @@ class NebulaIE(InfoExtractor): data = json.dumps({'email': username, 'password': password}).encode('utf8') response = self._download_json( 'https://api.watchnebula.com/api/v1/auth/login/', - data=data, fatal=False, video_id=video_id, + data=data, fatal=False, video_id=None, headers={ 'content-type': 'application/json', # Submitting the 'sessionid' cookie always causes a 403 on auth endpoint @@ -101,6 +105,19 @@ class NebulaIE(InfoExtractor): errnote='Authentication failed or rejected') if not response or not response.get('key'): self.raise_login_required() + + # save nebula token as cookie + self._set_cookie( + 'nebula.app', 'nebula-auth', + compat_urllib_parse_quote( + json.dumps({ + "apiToken": response["key"], + "isLoggingIn": False, + "isLoggingOut": False, + }, separators=(",", ":"))), + expire_time=int(time.time()) + 86400 * 365, + ) + return response['key'] def _retrieve_zype_api_key(self, page_url, display_id): @@ -139,8 +156,17 @@ class NebulaIE(InfoExtractor): 'Authorization': 'Token {access_token}'.format(access_token=access_token) }, note=note) - def _fetch_zype_access_token(self, video_id, nebula_token): - user_object = self._call_nebula_api('/auth/user/', video_id, nebula_token, note='Retrieving Zype access token') + def _fetch_zype_access_token(self, video_id): + try: + user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token') + except ExtractorError as exc: + # if 401, attempt credential auth and retry + if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401: + self._nebula_token = self._retrieve_nebula_auth() + user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token') + else: + raise + access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str) if not access_token: if try_get(user_object, lambda x: x['is_subscribed'], bool): @@ -162,9 +188,21 @@ class NebulaIE(InfoExtractor): if category.get('value'): return category['value'][0] + def _real_initialize(self): + # check cookie jar for valid token + nebula_cookies = self._get_cookies('https://nebula.app') + nebula_cookie = nebula_cookies.get('nebula-auth') + if nebula_cookie: + self.to_screen('Authenticating to Nebula with token from cookie jar') + nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value) + self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken') + + # try to authenticate using credentials if no valid token has been found + if not self._nebula_token: + self._nebula_token = self._retrieve_nebula_auth() + def _real_extract(self, url): display_id = self._match_id(url) - nebula_token = self._retrieve_nebula_auth(display_id) api_key = self._retrieve_zype_api_key(url, display_id) response = self._call_zype_api('/videos', {'friendly_title': display_id}, @@ -174,7 +212,7 @@ class NebulaIE(InfoExtractor): video_meta = response['response'][0] video_id = video_meta['_id'] - zype_access_token = self._fetch_zype_access_token(display_id, nebula_token=nebula_token) + zype_access_token = self._fetch_zype_access_token(display_id) channel_title = self._extract_channel_title(video_meta) @@ -187,13 +225,12 @@ class NebulaIE(InfoExtractor): 'title': video_meta.get('title'), 'description': video_meta.get('description'), 'timestamp': parse_iso8601(video_meta.get('published_at')), - 'thumbnails': [ - { - 'id': tn.get('name'), # this appears to be null - 'url': tn['url'], - 'width': tn.get('width'), - 'height': tn.get('height'), - } for tn in video_meta.get('thumbnails', [])], + 'thumbnails': [{ + 'id': tn.get('name'), # this appears to be null + 'url': tn['url'], + 'width': tn.get('width'), + 'height': tn.get('height'), + } for tn in video_meta.get('thumbnails', [])], 'duration': video_meta.get('duration'), 'channel': channel_title, 'uploader': channel_title, # we chose uploader = channel name