Compare commits

...

4 Commits

Author SHA1 Message Date
sepro
68a14d1859 Another typo 2023-11-19 23:01:12 +01:00
sepro
b7e61edfb9 typo 2023-11-19 22:50:50 +01:00
sepro
28dd7085c9 Address other comments 2023-11-19 22:47:24 +01:00
sepro
7fcdf62aac
Apply suggestions from code review
Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2023-11-19 22:29:17 +01:00

View File

@ -1,10 +1,11 @@
import itertools
import json
import urllib.error
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
int_or_none,
make_archive_id,
parse_iso8601,
smuggle_url,
@ -29,15 +30,12 @@ class NebulaBaseIE(InfoExtractor):
'https://nebula.tv/auth/login/', None,
'Logging in to Nebula', 'Login failed',
data=json.dumps({'email': username, 'password': password}).encode(),
headers={
'content-type': 'application/json',
'cookie': '' # 'sessionid' cookie causes 403
})
headers={'content-type': 'application/json'})
except ExtractorError as e:
if isinstance(e.cause, urllib.error.HTTPError) and e.cause.status == 400:
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
raise ExtractorError('Login failed: Invalid username or password', expected=True)
raise
self._api_token = response.get('key')
self._api_token = traverse_obj(response, ('key', {str}))
if not self._api_token:
raise ExtractorError('Login failed: No token')
@ -47,10 +45,10 @@ class NebulaBaseIE(InfoExtractor):
try:
return self._download_json(*args, **kwargs)
except ExtractorError as e:
if not isinstance(e.cause, urllib.error.HTTPError) or e.cause.status not in (401, 403):
if not isinstance(e.cause, HTTPError) or e.cause.status not in (401, 403):
raise
self.to_screen(f'Reautherizing with Nebula and retrying, '
f'because last API call resulted in error {e.cause.status}')
self.to_screen(
f'Reauthorizing with Nebula and retrying, because last API call resulted in error {e.cause.status}')
self._real_initialize()
if self._token:
kwargs.setdefault('headers', {})['Authorization'] = f'Bearer {self._token}'
@ -65,38 +63,37 @@ class NebulaBaseIE(InfoExtractor):
headers={'Authorization': f'Token {self._api_token}'} if self._api_token else None,
note='Authorizing to Nebula', data=b'')['token']
def _extract_formats(self, content_id, slug, fatal=False):
api_path = 'lessons' if content_id.startswith('lesson:') else 'video_episodes'
try:
fmts, subs = self._extract_m3u8_formats_and_subtitles(
f'https://content.api.nebula.app/{api_path}/{content_id}/manifest.m3u8',
slug, 'mp4', query={
'token': self._token,
'app_version': '23.10.0',
'platform': 'ios',
})
except ExtractorError as e:
if isinstance(e.cause, urllib.error.HTTPError) and e.cause.status == 401:
self.raise_login_required('This video is only available for users with an active subscription')
if fatal or not isinstance(e.cause, urllib.error.HTTPError) or e.cause.status != 403:
def _extract_formats(self, content_id, slug):
for retry in (False, True):
try:
fmts, subs = self._extract_m3u8_formats_and_subtitles(
f'https://content.api.nebula.app/{content_id.split(":")[0]}s/{content_id}/manifest.m3u8',
slug, 'mp4', query={
'token': self._token,
'app_version': '23.10.0',
'platform': 'ios',
})
return {'formats': fmts, 'subtitles': subs}
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
self.raise_login_required()
if not retry and isinstance(e.cause, HTTPError) and e.cause.status == 403:
self.to_screen('Reauthorizing with Nebula and retrying, because fetching video resulted in error')
self._real_initialize()
continue
raise
self.to_screen('Reautherizing with Nebula and retrying, because fetching video resulted in error')
self._real_initialize()
return self._extract_formats(content_id, slug, fatal=True)
return {'formats': fmts, 'subtitles': subs}
def _extract_video_metadata(self, episode):
channel_url = traverse_obj(
episode, (('channel_slug', 'class_slug'), {lambda x: urljoin('https://nebula.tv/', x)}), get_all=False)
return {
'id': episode['id'].split(':', 1)[-1],
'id': episode['id'].partition(':')[2],
**traverse_obj(episode, {
'display_id': 'slug',
'title': 'title',
'description': 'description',
'timestamp': ('published_at', {parse_iso8601}),
'duration': 'duration',
'duration': ('duration', {int_or_none}),
'channel_id': 'channel_slug',
'uploader_id': 'channel_slug',
'channel': 'channel_title',
@ -104,7 +101,7 @@ class NebulaBaseIE(InfoExtractor):
'series': 'channel_title',
'creator': 'channel_title',
'thumbnail': ('images', 'thumbnail', 'src', {url_or_none}),
'episode_number': 'order',
'episode_number': ('order', {int_or_none}),
# Old code was wrongly setting extractor_key from NebulaSubscriptionsIE
'_old_archive_ids': ('zype_id', {lambda x: [
make_archive_id(NebulaIE, x), make_archive_id(NebulaSubscriptionsIE, x)] if x else None}),
@ -239,6 +236,7 @@ class NebulaIE(NebulaBaseIE):
class NebulaClassIE(NebulaBaseIE):
IE_NAME = 'nebula:class'
_VALID_URL = rf'{_BASE_URL_RE}/(?P<id>[-\w]+)/(?P<ep>\d+)'
_TESTS = [{
'url': 'https://nebula.tv/copyright-for-fun-and-profit/14',
@ -360,7 +358,7 @@ class NebulaChannelIE(NebulaBaseIE):
if not next_url:
break
def _generate_playlist_entries_class(self, channel):
def _generate_class_entries(self, channel):
for lesson in channel['lessons']:
metadata = self._extract_video_metadata(lesson)
yield self.url_result(smuggle_url(
@ -374,7 +372,7 @@ class NebulaChannelIE(NebulaBaseIE):
collection_slug, note='Retrieving channel')
if channel.get('type') == 'class':
entries = self._generate_playlist_entries_class(channel)
entries = self._generate_class_entries(channel)
else:
entries = self._generate_playlist_entries(channel['id'], collection_slug)