From 6a188aed91fb0a1b9d62f20377015b8fd2c69762 Mon Sep 17 00:00:00 2001 From: doe1080 <98906116+doe1080@users.noreply.github.com> Date: Sun, 12 Jul 2026 09:48:16 +0900 Subject: [PATCH] [ie/showroom] Rework extractors (#17142) Closes #2072, Closes #7297 Authored by: doe1080 --- yt_dlp/extractor/_extractors.py | 5 +- yt_dlp/extractor/showroomlive.py | 172 ++++++++++++++++++++----------- 2 files changed, 117 insertions(+), 60 deletions(-) diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index 58a8ae4ad2..7a2d71b3de 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1718,7 +1718,10 @@ from .shahid import ( from .sharepoint import SharePointIE from .shemaroome import ShemarooMeIE from .shiey import ShieyIE -from .showroomlive import ShowRoomLiveIE +from .showroomlive import ( + ShowRoomLiveIE, + ShowRoomVodIE, +) from .sibnet import SibnetEmbedIE from .simplecast import ( SimplecastEpisodeIE, diff --git a/yt_dlp/extractor/showroomlive.py b/yt_dlp/extractor/showroomlive.py index 999b1f402c..4629a90d3c 100644 --- a/yt_dlp/extractor/showroomlive.py +++ b/yt_dlp/extractor/showroomlive.py @@ -1,80 +1,134 @@ +import datetime as dt +import json + from .common import InfoExtractor from ..utils import ( - ExtractorError, + UserNotLive, + clean_html, + extract_attributes, int_or_none, - urljoin, + str_or_none, + url_or_none, +) +from ..utils.traversal import ( + find_element, + require, + traverse_obj, ) class ShowRoomLiveIE(InfoExtractor): - _WORKING = False - _VALID_URL = r'https?://(?:www\.)?showroom-live\.com/(?!onlive|timetable|event|campaign|news|ranking|room)(?P[^/?#&]+)' - _TEST = { - 'url': 'https://www.showroom-live.com/48_Nana_Okada', + IE_NAME = 'showroom:live' + IE_DESC = 'SHOWROOM' + + _VALID_URL = r'https?://(?:www\.)?showroom-live\.com/r/(?P[\w-]+)' + _TESTS = [{ + 'url': 'https://www.showroom-live.com/r/48_Yui_Oguri', 'only_matching': True, - } + }] def _real_extract(self, url): broadcaster_id = self._match_id(url) + webpage = self._download_webpage( + url, broadcaster_id, headers={'Accept-Language': 'ja'}) + nuxt_data = self._search_nuxt_json(webpage, broadcaster_id)['data'] - webpage = self._download_webpage(url, broadcaster_id) + cookies = self._get_cookies(url) + sr_id = traverse_obj(cookies, ('sr_id', 'value', {str}, filter)) + if not sr_id: + self.raise_login_required() - room_id = self._search_regex( - (r'SrGlobal\.roomId\s*=\s*(\d+)', - r'(?:profile|room)\?room_id\=(\d+)'), webpage, 'room_id') + room_profile = traverse_obj(nuxt_data, ( + f'roomProfile-{broadcaster_id}-{sr_id}', {dict})) + start_timestamp = traverse_obj(room_profile, ('current_live_started_at', {int_or_none})) + is_live = traverse_obj(room_profile, ('is_onlive', {bool})) - room = self._download_json( - urljoin(url, f'/api/room/profile?room_id={room_id}'), - broadcaster_id) + if not is_live: + if start_timestamp: + start_time = dt.datetime.fromtimestamp( + start_timestamp, dt.timezone.utc, + ).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z') - is_live = room.get('is_onlive') - if is_live is not True: - raise ExtractorError(f'{broadcaster_id} is offline', expected=True) + self.raise_no_formats( + f'Next livestream is scheduled to start at {start_time}', expected=True) - uploader = room.get('performer_name') or broadcaster_id - title = room.get('room_name') or room.get('main_name') or uploader + return { + 'id': broadcaster_id, + 'live_status': 'is_upcoming', + 'release_timestamp': start_timestamp, + } + raise UserNotLive(video_id=broadcaster_id) + + room_id = traverse_obj(room_profile, ('room_id', {str_or_none})) + room_name = traverse_obj(room_profile, ( + ('room_name', 'main_name'), {clean_html}, filter, any)) streaming_url_list = self._download_json( - urljoin(url, f'/api/live/streaming_url?room_id={room_id}'), - broadcaster_id)['streaming_url_list'] - - formats = [] - for stream in streaming_url_list: - stream_url = stream.get('url') - if not stream_url: - continue - stream_type = stream.get('type') - if stream_type == 'hls': - m3u8_formats = self._extract_m3u8_formats( - stream_url, broadcaster_id, ext='mp4', m3u8_id='hls', - live=True) - for f in m3u8_formats: - f['quality'] = int_or_none(stream.get('quality', 100)) - formats.extend(m3u8_formats) - elif stream_type == 'rtmp': - stream_name = stream.get('stream_name') - if not stream_name: - continue - formats.append({ - 'url': stream_url, - 'play_path': stream_name, - 'page_url': url, - 'player_url': 'https://www.showroom-live.com/assets/swf/v3/ShowRoomLive.swf', - 'rtmp_live': True, - 'ext': 'flv', - 'format_id': 'rtmp', - 'format_note': stream.get('label'), - 'quality': int_or_none(stream.get('quality', 100)), - }) + 'https://www.showroom-live.com/api/live/streaming_url', + broadcaster_id, query={'room_id': room_id}) + m3u8_url = traverse_obj(streaming_url_list, ( + 'streaming_url_list', lambda _, v: v['type'] == 'hls_all', + 'url', {url_or_none}, any, {require('m3u8 URL')})) return { - 'id': str(room.get('live_id') or broadcaster_id), - 'title': title, - 'description': room.get('description'), - 'timestamp': int_or_none(room.get('current_live_started_at')), - 'uploader': uploader, - 'uploader_id': broadcaster_id, - 'view_count': int_or_none(room.get('view_num')), - 'formats': formats, - 'is_live': True, + 'title': room_name, + 'channel': room_name, + 'channel_id': broadcaster_id, + 'formats': self._extract_m3u8_formats(m3u8_url, broadcaster_id, 'mp4'), + 'is_live': is_live, + 'release_timestamp': start_timestamp, + **traverse_obj(room_profile, { + 'id': ('live_id', {str_or_none}), + 'channel_follower_count': ('follower_num', {int_or_none}), + 'channel_is_verified': ('is_official', {bool}), + 'description': ('description', {clean_html}, filter), + 'genres': ('genre_name', {clean_html}, filter, all, filter), + 'tags': ('live_tags', ..., 'name', {clean_html}, filter, all, filter), + 'thumbnail': ('image_square', {url_or_none}), + 'view_count': ('view_num', {int_or_none}), + }), + } + + +class ShowRoomVodIE(InfoExtractor): + IE_NAME = 'showroom:vod' + + _VALID_URL = r'https?://(?:www\.)?showroom-live\.com/episode/watch\?(?:[^#]+&)?id=(?P\w+)' + _TESTS = [{ + 'url': 'https://www.showroom-live.com/episode/watch?id=214', + 'info_dict': { + 'id': '214', + 'ext': 'mp4', + 'title': 'aaa', + }, + }] + + def _real_extract(self, url): + episode_id = self._match_id(url) + webpage = self._download_webpage(url, episode_id) + episode_data = traverse_obj(webpage, ( + {find_element(id='episode-data', html=True)}, + {extract_attributes}, 'data-episode', {json.loads}, {dict})) + + streaming_url_list = self._download_json( + 'https://www.showroom-live.com/api/episode/streaming_url', + episode_id, query={'episode_id': episode_id}) + m3u8_url = traverse_obj(streaming_url_list, ( + 'streaming_url_list', 'hls_all', + 'hls_all', {url_or_none}, {require('m3u8 URL')})) + + return { + 'id': episode_id, + 'formats': self._extract_m3u8_formats(m3u8_url, episode_id, 'mp4'), + 'thumbnail': traverse_obj(streaming_url_list, ('thumbnail_url', {url_or_none})), + **traverse_obj(episode_data, { + 'title': ('title', {clean_html}, filter), + 'description': ('description', {clean_html}, filter), + 'duration': ('video_time', {int_or_none}), + 'timestamp': ('display_started_at', {int_or_none}), + }), + **traverse_obj(episode_data, ('series', { + 'series': ('name', {clean_html}, filter), + 'series_id': ('id', {str_or_none}), + })), }