from __future__ import unicode_literals\r
\r
import json\r
+import time\r
\r
+from urllib.error import HTTPError\r
from .common import InfoExtractor\r
-from ..compat import compat_str\r
+from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote\r
from ..utils import (\r
ExtractorError,\r
parse_iso8601,\r
\r
class NebulaIE(InfoExtractor):\r
\r
- _VALID_URL = r'https?://(?:www\.)?watchnebula\.com/videos/(?P<id>[-\w]+)'\r
+ _VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'\r
_TESTS = [\r
{\r
- 'url': 'https://watchnebula.com/videos/that-time-disney-remade-beauty-and-the-beast',\r
+ 'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',\r
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',\r
'info_dict': {\r
'id': '5c271b40b13fd613090034fd',\r
'skip': 'All Nebula content requires authentication',\r
},\r
{\r
- 'url': 'https://watchnebula.com/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',\r
+ 'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',\r
'md5': '6d4edd14ce65720fa63aba5c583fb328',\r
'info_dict': {\r
'id': '5e7e78171aaf320001fbd6be',\r
'skip': 'All Nebula content requires authentication',\r
},\r
{\r
- 'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',\r
+ 'url': 'https://nebula.app/videos/money-episode-1-the-draw',\r
'md5': '8c7d272910eea320f6f8e6d3084eecf5',\r
'info_dict': {\r
'id': '5e779ebdd157bc0001d1c75a',\r
},\r
'skip': 'All Nebula content requires authentication',\r
},\r
+ {\r
+ 'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',\r
+ 'only_matching': True,\r
+ },\r
]\r
_NETRC_MACHINE = 'watchnebula'\r
\r
- def _retrieve_nebula_auth(self, video_id):\r
+ _nebula_token = None\r
+\r
+ def _retrieve_nebula_auth(self):\r
"""\r
Log in to Nebula, and returns a Nebula API token\r
"""\r
data = json.dumps({'email': username, 'password': password}).encode('utf8')\r
response = self._download_json(\r
'https://api.watchnebula.com/api/v1/auth/login/',\r
- data=data, fatal=False, video_id=video_id,\r
+ data=data, fatal=False, video_id=None,\r
headers={\r
'content-type': 'application/json',\r
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint\r
errnote='Authentication failed or rejected')\r
if not response or not response.get('key'):\r
self.raise_login_required()\r
+\r
+ # save nebula token as cookie\r
+ self._set_cookie(\r
+ 'nebula.app', 'nebula-auth',\r
+ compat_urllib_parse_quote(\r
+ json.dumps({\r
+ "apiToken": response["key"],\r
+ "isLoggingIn": False,\r
+ "isLoggingOut": False,\r
+ }, separators=(",", ":"))),\r
+ expire_time=int(time.time()) + 86400 * 365,\r
+ )\r
+\r
return response['key']\r
\r
def _retrieve_zype_api_key(self, page_url, display_id):\r
'Authorization': 'Token {access_token}'.format(access_token=access_token)\r
}, note=note)\r
\r
- def _fetch_zype_access_token(self, video_id, nebula_token):\r
- user_object = self._call_nebula_api('/auth/user/', video_id, nebula_token, note='Retrieving Zype access token')\r
+ def _fetch_zype_access_token(self, video_id):\r
+ try:\r
+ user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')\r
+ except ExtractorError as exc:\r
+ # if 401, attempt credential auth and retry\r
+ if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:\r
+ self._nebula_token = self._retrieve_nebula_auth()\r
+ user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')\r
+ else:\r
+ raise\r
+\r
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)\r
if not access_token:\r
if try_get(user_object, lambda x: x['is_subscribed'], bool):\r
if category.get('value'):\r
return category['value'][0]\r
\r
+ def _real_initialize(self):\r
+ # check cookie jar for valid token\r
+ nebula_cookies = self._get_cookies('https://nebula.app')\r
+ nebula_cookie = nebula_cookies.get('nebula-auth')\r
+ if nebula_cookie:\r
+ self.to_screen('Authenticating to Nebula with token from cookie jar')\r
+ nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)\r
+ self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')\r
+\r
+ # try to authenticate using credentials if no valid token has been found\r
+ if not self._nebula_token:\r
+ self._nebula_token = self._retrieve_nebula_auth()\r
+\r
def _real_extract(self, url):\r
display_id = self._match_id(url)\r
- nebula_token = self._retrieve_nebula_auth(display_id)\r
api_key = self._retrieve_zype_api_key(url, display_id)\r
\r
response = self._call_zype_api('/videos', {'friendly_title': display_id},\r
video_meta = response['response'][0]\r
\r
video_id = video_meta['_id']\r
- zype_access_token = self._fetch_zype_access_token(display_id, nebula_token=nebula_token)\r
+ zype_access_token = self._fetch_zype_access_token(display_id)\r
\r
channel_title = self._extract_channel_title(video_meta)\r
\r
'title': video_meta.get('title'),\r
'description': video_meta.get('description'),\r
'timestamp': parse_iso8601(video_meta.get('published_at')),\r
- 'thumbnails': [\r
- {\r
- 'id': tn.get('name'), # this appears to be null\r
- 'url': tn['url'],\r
- 'width': tn.get('width'),\r
- 'height': tn.get('height'),\r
- } for tn in video_meta.get('thumbnails', [])],\r
+ 'thumbnails': [{\r
+ 'id': tn.get('name'), # this appears to be null\r
+ 'url': tn['url'],\r
+ 'width': tn.get('width'),\r
+ 'height': tn.get('height'),\r
+ } for tn in video_meta.get('thumbnails', [])],\r
'duration': video_meta.get('duration'),\r
'channel': channel_title,\r
'uploader': channel_title, # we chose uploader = channel name\r