From f5270705e816a24caef7357a7ce8e17471899d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0x=E2=88=85?= Date: Sat, 20 Dec 2025 07:21:09 +0530 Subject: [PATCH] [ie/nebula:season] Add extractor (#15347) Closes #15343 Authored by: 0xvd, bashonly Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com> --- yt_dlp/extractor/_extractors.py | 1 + yt_dlp/extractor/nebula.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index 62d431e640..2cf475602d 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1279,6 +1279,7 @@ from .nebula import ( NebulaChannelIE, NebulaClassIE, NebulaIE, + NebulaSeasonIE, NebulaSubscriptionsIE, ) from .nekohacker import NekoHackerIE diff --git a/yt_dlp/extractor/nebula.py b/yt_dlp/extractor/nebula.py index 6ced19d6a2..93d01092d6 100644 --- a/yt_dlp/extractor/nebula.py +++ b/yt_dlp/extractor/nebula.py @@ -478,3 +478,56 @@ class NebulaChannelIE(NebulaBaseIE): playlist_id=collection_slug, playlist_title=channel.get('title'), playlist_description=channel.get('description')) + + +class NebulaSeasonIE(NebulaBaseIE): + IE_NAME = 'nebula:season' + _VALID_URL = rf'{_BASE_URL_RE}/(?P[\w-]+)/season/(?P\d+)' + _TESTS = [{ + 'url': 'https://nebula.tv/jetlag/season/15', + 'info_dict': { + 'id': 'jetlag_15', + 'title': 'Tag: All Stars', + 'description': 'md5:5aa5b8abf3de71756448dc44ffebb674', + }, + 'playlist_count': 8, + }, { + 'url': 'https://nebula.tv/jetlag/season/14', + 'info_dict': { + 'id': 'jetlag_14', + 'title': 'Snake', + 'description': 'md5:6da9040f1c2ac559579738bfb6919d1e', + }, + 'playlist_count': 8, + }] + + def _build_url_result(self, item): + url = ( + traverse_obj(item, ('share_url', {url_or_none})) + or urljoin('https://nebula.tv/', item.get('app_path')) + or f'https://nebula.tv/videos/{item["slug"]}') + return self.url_result( + smuggle_url(url, {'id': item['id']}), + NebulaIE, url_transparent=True, + **self._extract_video_metadata(item)) + + def _entries(self, data): + for episode in traverse_obj(data, ('episodes', lambda _, v: v['video']['id'], 'video')): + yield self._build_url_result(episode) + for extra in traverse_obj(data, ('extras', ..., 'items', lambda _, v: v['id'])): + yield self._build_url_result(extra) + for trailer in traverse_obj(data, ('trailers', lambda _, v: v['id'])): + yield self._build_url_result(trailer) + + def _real_extract(self, url): + series, season_id = self._match_valid_url(url).group('series', 'season_number') + playlist_id = f'{series}_{season_id}' + data = self._call_api( + f'https://content.api.nebula.app/content/{series}/season/{season_id}', playlist_id) + + return self.playlist_result( + self._entries(data), playlist_id, + **traverse_obj(data, { + 'title': ('title', {str}), + 'description': ('description', {str}), + }))