diff --git a/youtube_dl/extractor/extractors.py b/youtube_dl/extractor/extractors.py index 3da5f8020..aaad0b6b0 100644 --- a/youtube_dl/extractor/extractors.py +++ b/youtube_dl/extractor/extractors.py @@ -1328,7 +1328,10 @@ from .trovo import ( from .trunews import TruNewsIE from .trutv import TruTVIE from .tube8 import Tube8IE -from .tubitv import TubiTvIE +from .tubitv import ( + TubiTvIE, + TubiTvShowIE, +) from .tumblr import TumblrIE from .tunein import ( TuneInClipIE, diff --git a/youtube_dl/extractor/tubitv.py b/youtube_dl/extractor/tubitv.py index c8d7a33e7..72fc512b0 100644 --- a/youtube_dl/extractor/tubitv.py +++ b/youtube_dl/extractor/tubitv.py @@ -6,6 +6,7 @@ from ..utils import ( ExtractorError, get_element_by_id, int_or_none, + join_nonempty, js_to_json, merge_dicts, parse_age_limit, @@ -193,3 +194,80 @@ class TubiTvIE(InfoExtractor): 'series': ('title', T(strip_or_none)), # 'series_id': ('id', T(compat_str)), }), get_all=False), info) + + +class TubiTvShowIE(InfoExtractor): + IE_NAME = 'tubitv:series' + _VALID_URL = r'https?://(?:www\.)?tubitv\.com/series/\d+/(?P[^/?#]+)(?:/season-(?P\d+))?' + _TESTS = [{ + 'url': 'https://tubitv.com/series/3936/the-joy-of-painting-with-bob-ross?start=true', + 'playlist_mincount': 390, + 'info_dict': { + 'id': 'the-joy-of-painting-with-bob-ross', + }, + }, { + 'url': 'https://tubitv.com/series/3936/the-joy-of-painting-with-bob-ross/season-1', + 'playlist_count': 13, + 'info_dict': { + 'id': 'the-joy-of-painting-with-bob-ross-season-1', + }, + }, { + 'url': 'https://tubitv.com/series/3936/the-joy-of-painting-with-bob-ross/season-3', + 'playlist_count': 13, + 'info_dict': { + 'id': 'the-joy-of-painting-with-bob-ross-season-3', + }, + }] + + def _real_extract(self, url): + playlist_id, selected_season = self._match_valid_url(url).group( + 'show_name', 'season') + + def entries(s_url, playlist_id, selected_season_num): + + def get_season_data(s_num, fatal=False): + if s_num is None: + url, s_id = s_url, playlist_id + else: + url = '%s/season-%d' % (s_url, s_num) + s_id = '%s-season-%d' % (playlist_id, s_num) + webpage = self._download_webpage(url, s_id, fatal=fatal) + data = self._search_json( + r'window\s*\.\s*__data\s*=', webpage or '', 'data', s_id, + transform_source=js_to_json, default={}) + return data['video'] if fatal else data.get('video', {}) + + data = get_season_data(None, fatal=True) + # The {series_id}.seasons JSON may lack some episodes that are available + # Iterate over the season numbers instead [1] + # 1. https://github.com/yt-dlp/yt-dlp/issues/11170#issuecomment-2399918777 + seasons = ( + traverse_obj(data, ( + 'byId', lambda _, v: v['type'] == 's', 'seasons', Ellipsis, + 'number', T(int_or_none))) + if selected_season is None + else [selected_season]) + + unavail_cnt = 0 + select_episodes = lambda _, v: v['type'] == 'v' + for season_number in seasons: + if not data: + data = get_season_data(season_number) + + unavail_cnt += len(traverse_obj(data, ('byId', select_episodes, 'policy_match', T(lambda m: (not m) or None)))) + + for episode_id, episode in traverse_obj(data, ('byId', select_episodes, T(lambda e: (e['id'], e)))): + yield merge_dicts(self.url_result( + 'https://tubitv.com/tv-shows/{0}/'.format(episode_id), TubiTvIE.ie_key(), episode_id), { + 'season_number': season_number, + 'episode_number': int_or_none(episode.get('num')), + }) + + data = None + + if unavail_cnt > 0: + self.report_warning('%d items were marked as unavailable: check that the desired content is available or provide login parameters if needed' % unavail_cnt) + + return self.playlist_result( + entries(url, playlist_id, int_or_none(selected_season)), + join_nonempty(playlist_id, selected_season, delim='-season-'))