]> jfr.im git - yt-dlp.git/commitdiff
[nebula] Add extractor (watchnebula.com) (#122)
authorHenrik Heimbuerger <redacted>
Fri, 9 Apr 2021 11:25:33 +0000 (13:25 +0200)
committerpukkandan <redacted>
Fri, 9 Apr 2021 11:27:38 +0000 (16:57 +0530)
Authored by: hheimbuerger

yt_dlp/extractor/extractors.py
yt_dlp/extractor/nebula.py [new file with mode: 0644]

index 00658c8569e0aae13e5ec2fd58b698286d5c2c72..b9e3c4ad3598deaad4d66cf785b63bc51c6ff923 100644 (file)
     NJoyEmbedIE,
 )
 from .ndtv import NDTVIE
-from .netzkino import NetzkinoIE
+from .nebula import NebulaIE
 from .nerdcubed import NerdCubedFeedIE
+from .netzkino import NetzkinoIE
 from .neteasemusic import (
     NetEaseMusicIE,
     NetEaseMusicAlbumIE,
diff --git a/yt_dlp/extractor/nebula.py b/yt_dlp/extractor/nebula.py
new file mode 100644 (file)
index 0000000..3e1b2ef
--- /dev/null
@@ -0,0 +1,197 @@
+# coding: utf-8\r
+from __future__ import unicode_literals\r
+\r
+import json\r
+\r
+from .common import InfoExtractor\r
+from ..compat import compat_str\r
+from ..utils import (\r
+    ExtractorError,\r
+    parse_iso8601,\r
+    try_get,\r
+    urljoin,\r
+)\r
+\r
+\r
+class NebulaIE(InfoExtractor):\r
+\r
+    _VALID_URL = r'https?://(?:www\.)?watchnebula\.com/videos/(?P<id>[-\w]+)'\r
+    _TESTS = [\r
+        {\r
+            'url': 'https://watchnebula.com/videos/that-time-disney-remade-beauty-and-the-beast',\r
+            'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',\r
+            'info_dict': {\r
+                'id': '5c271b40b13fd613090034fd',\r
+                'ext': 'mp4',\r
+                'title': 'That Time Disney Remade Beauty and the Beast',\r
+                'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',\r
+                'upload_date': '20180731',\r
+                'timestamp': 1533009600,\r
+                'channel': 'Lindsay Ellis',\r
+                'uploader': 'Lindsay Ellis',\r
+            },\r
+            'params': {\r
+                'usenetrc': True,\r
+            },\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
+            'md5': '6d4edd14ce65720fa63aba5c583fb328',\r
+            'info_dict': {\r
+                'id': '5e7e78171aaf320001fbd6be',\r
+                'ext': 'mp4',\r
+                'title': 'Landing Craft - How The Allies Got Ashore',\r
+                'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',\r
+                'upload_date': '20200327',\r
+                'timestamp': 1585348140,\r
+                'channel': 'The Logistics of D-Day',\r
+                'uploader': 'The Logistics of D-Day',\r
+            },\r
+            'params': {\r
+                'usenetrc': True,\r
+            },\r
+            'skip': 'All Nebula content requires authentication',\r
+        },\r
+        {\r
+            'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',\r
+            'md5': '8c7d272910eea320f6f8e6d3084eecf5',\r
+            'info_dict': {\r
+                'id': '5e779ebdd157bc0001d1c75a',\r
+                'ext': 'mp4',\r
+                'title': 'Episode 1: The Draw',\r
+                'description': r'contains:There’s free money on offer… if the players can all work together.',\r
+                'upload_date': '20200323',\r
+                'timestamp': 1584980400,\r
+                'channel': 'Tom Scott Presents: Money',\r
+                'uploader': 'Tom Scott Presents: Money',\r
+            },\r
+            'params': {\r
+                'usenetrc': True,\r
+            },\r
+            'skip': 'All Nebula content requires authentication',\r
+        },\r
+    ]\r
+    _NETRC_MACHINE = 'watchnebula'\r
+\r
+    def _retrieve_nebula_auth(self, video_id):\r
+        """\r
+        Log in to Nebula, and returns a Nebula API token\r
+        """\r
+\r
+        username, password = self._get_login_info()\r
+        if not (username and password):\r
+            self.raise_login_required()\r
+\r
+        self.report_login()\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
+            headers={\r
+                'content-type': 'application/json',\r
+                # Submitting the 'sessionid' cookie always causes a 403 on auth endpoint\r
+                'cookie': ''\r
+            },\r
+            note='Authenticating to Nebula with supplied credentials',\r
+            errnote='Authentication failed or rejected')\r
+        if not response or not response.get('key'):\r
+            self.raise_login_required()\r
+        return response['key']\r
+\r
+    def _retrieve_zype_api_key(self, page_url, display_id):\r
+        """\r
+        Retrieves the Zype API key\r
+        """\r
+\r
+        # Find the js that has the API key from the webpage and download it\r
+        webpage = self._download_webpage(page_url, video_id=display_id)\r
+        main_script_relpath = self._search_regex(\r
+            r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,\r
+            group='script_relpath', name='script relative path', fatal=True)\r
+        main_script_abspath = urljoin(page_url, main_script_relpath)\r
+        main_script = self._download_webpage(main_script_abspath, video_id=display_id,\r
+                                             note='Retrieving Zype API key')\r
+\r
+        api_key = self._search_regex(\r
+            r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,\r
+            group='api_key', name='API key', fatal=True)\r
+\r
+        return api_key\r
+\r
+    def _call_zype_api(self, path, params, video_id, api_key, note):\r
+        """\r
+        A helper for making calls to the Zype API.\r
+        """\r
+        query = {'api_key': api_key, 'per_page': 1}\r
+        query.update(params)\r
+        return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)\r
+\r
+    def _call_nebula_api(self, path, video_id, access_token, note):\r
+        """\r
+        A helper for making calls to the Nebula API.\r
+        """\r
+        return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={\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
+        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
+                # TODO: Reimplement the same Zype token polling the Nebula frontend implements\r
+                # see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532\r
+                raise ExtractorError(\r
+                    'Unable to extract Zype access token from Nebula API authentication endpoint. '\r
+                    'Open an arbitrary video in a browser with this account to generate a token',\r
+                    expected=True)\r
+            raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')\r
+        return access_token\r
+\r
+    def _extract_channel_title(self, video_meta):\r
+        # TODO: Implement the API calls giving us the channel list,\r
+        # so that we can do the title lookup and then figure out the channel URL\r
+        categories = video_meta.get('categories', []) if video_meta else []\r
+        # the channel name is the value of the first category\r
+        for category in categories:\r
+            if category.get('value'):\r
+                return category['value'][0]\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
+                                       display_id, api_key, note='Retrieving metadata from Zype')\r
+        if len(response.get('response') or []) != 1:\r
+            raise ExtractorError('Unable to find video on Zype API')\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
+\r
+        channel_title = self._extract_channel_title(video_meta)\r
+\r
+        return {\r
+            'id': video_id,\r
+            'display_id': display_id,\r
+            '_type': 'url_transparent',\r
+            'ie_key': 'Zype',\r
+            'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),\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
+            'duration': video_meta.get('duration'),\r
+            'channel': channel_title,\r
+            'uploader': channel_title,  # we chose uploader = channel name\r
+            # TODO: uploader_url, channel_id, channel_url\r
+        }\r