diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index 42034275b..997c21c6c 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -149,8 +149,6 @@ from .arte import ( ArteTVCategoryIE, ) from .arnes import ArnesIE -from .asobichannel import AsobiChannelIE, AsobiChannelTagURLIE -from .asobistage import AsobiStageIE from .atresplayer import AtresPlayerIE from .atscaleconf import AtScaleConfEventIE from .atvat import ATVAtIE diff --git a/yt_dlp/extractor/asobichannel.py b/yt_dlp/extractor/asobichannel.py deleted file mode 100644 index e3479ede9..000000000 --- a/yt_dlp/extractor/asobichannel.py +++ /dev/null @@ -1,168 +0,0 @@ -from .common import InfoExtractor -from ..utils import ( - ExtractorError, - clean_html, - merge_dicts, - parse_iso8601, - url_or_none, -) -from ..utils.traversal import traverse_obj - - -class AsobiChannelBaseIE(InfoExtractor): - _MICROCMS_HEADER = {'X-MICROCMS-API-KEY': 'qRaKehul9AHU8KtL0dnq1OCLKnFec6yrbcz3'} - - def _extract_info(self, metadata): - return traverse_obj(metadata, { - 'id': ('id', {str}), - 'title': ('title', {str}), - 'description': ('body', {clean_html}), - 'thumbnail': ('contents', 'video_thumb', 'url', {url_or_none}), - 'timestamp': ('publishedAt', {parse_iso8601}), - 'modified_timestamp': ('updatedAt', {parse_iso8601}), - 'channel': ('channel', 'name', {str}), - 'channel_id': ('channel', 'id', {str}), - }) - - -class AsobiChannelIE(AsobiChannelBaseIE): - IE_NAME = 'asobichannel' - IE_DESC = 'ASOBI CHANNEL' - - _VALID_URL = r'https?://asobichannel\.asobistore\.jp/watch/(?P[\w-]+)' - _TESTS = [{ - 'url': 'https://asobichannel.asobistore.jp/watch/1ypp48qd32p', - 'md5': '39df74e872afe032c4eb27b89144fc92', - 'info_dict': { - 'id': '1ypp48qd32p', - 'ext': 'mp4', - 'title': 'アイドルマスター ミリオンライブ! 765プロch 原っぱ通信 #1', - 'description': 'md5:b930bd2199c9b2fd75951ce4aaa7efd2', - 'thumbnail': 'https://images.microcms-assets.io/assets/d2420de4b9194e11beb164f99edb1f95/a8e6f84119f54eb9ab4ce16729239905/%E3%82%B5%E3%83%A0%E3%83%8D%20(1).png', - 'timestamp': 1697098247, - 'upload_date': '20231012', - 'modified_timestamp': 1698381162, - 'modified_date': '20231027', - 'channel': 'アイドルマスター', - 'channel_id': 'idolmaster', - }, - }, { - 'url': 'https://asobichannel.asobistore.jp/watch/redigiwnjzqj', - 'md5': '229fa8fb5c591c75ce8c37a497f113f6', - 'info_dict': { - 'id': 'redigiwnjzqj', - 'ext': 'mp4', - 'title': '【おまけ放送】アイドルマスター ミリオンライブ! 765プロch 原っぱ通信 #1', - 'description': 'md5:7d9cd35fb54425a6967822bd564ea2d9', - 'thumbnail': 'https://images.microcms-assets.io/assets/d2420de4b9194e11beb164f99edb1f95/20e5c1d6184242eebc2512a5dec59bf0/P1_%E5%8E%9F%E3%81%A3%E3%81%B1%E3%82%B5%E3%83%A0%E3%83%8D.png', - 'modified_timestamp': 1697797125, - 'modified_date': '20231020', - 'timestamp': 1697261769, - 'upload_date': '20231014', - 'channel': 'アイドルマスター', - 'channel_id': 'idolmaster', - }, - }] - - _survapi_header = None - - def _real_initialize(self): - token = self._download_json( - 'https://asobichannel-api.asobistore.jp/api/v1/vspf/token', None, - note='Retrieving API token') - self._survapi_header = {'Authorization': f'Bearer {token}'} - - def _process_vod(self, video_id, metadata): - content_id = metadata['contents']['video_id'] - - vod_data = self._download_json( - f'https://survapi.channel.or.jp/proxy/v1/contents/{content_id}/get_by_cuid', video_id, - headers=self._survapi_header, note='Downloading vod data') - - return { - 'formats': self._extract_m3u8_formats(vod_data['ex_content']['streaming_url'], video_id), - } - - def _process_live(self, video_id, metadata): - content_id = metadata['contents']['video_id'] - event_data = self._download_json( - f'https://survapi.channel.or.jp/ex/events/{content_id}?embed=channel', video_id, - headers=self._survapi_header, note='Downloading event data') - - player_type = traverse_obj(event_data, ('data', 'Player_type', {str})) - if player_type == 'poster': - self.raise_no_formats('Live event has not yet started', expected=True) - live_status = 'is_upcoming' - formats = [] - elif player_type == 'player': - live_status = 'is_live' - formats = self._extract_m3u8_formats( - event_data['data']['Channel']['Custom_live_url'], video_id, live=True) - else: - raise ExtractorError('Unsupported player type {player_type!r}') - - return { - 'release_timestamp': traverse_obj(metadata, ('period', 'start', {parse_iso8601})), - 'live_status': live_status, - 'formats': formats, - } - - def _real_extract(self, url): - video_id = self._match_id(url) - - metadata = self._download_json( - f'https://channel.microcms.io/api/v1/media/{video_id}', video_id, - headers=self._MICROCMS_HEADER) - - info = self._extract_info(metadata) - - video_type = traverse_obj(metadata, ('contents', 'video_type', 0, {str})) - if video_type == 'VOD': - return merge_dicts(info, self._process_vod(video_id, metadata)) - if video_type == 'LIVE': - return merge_dicts(info, self._process_live(video_id, metadata)) - - raise ExtractorError(f'Unexpected video type {video_type!r}') - - -class AsobiChannelTagURLIE(AsobiChannelBaseIE): - IE_NAME = 'asobichannel:tag' - IE_DESC = 'ASOBI CHANNEL' - - _VALID_URL = r'https?://asobichannel\.asobistore\.jp/tag/(?P[a-z0-9-_]+)' - _TESTS = [{ - 'url': 'https://asobichannel.asobistore.jp/tag/bjhh-nbcja', - 'info_dict': { - 'id': 'bjhh-nbcja', - 'title': 'アイドルマスター ミリオンライブ! 765プロch 原っぱ通信', - }, - 'playlist_mincount': 16, - }, { - 'url': 'https://asobichannel.asobistore.jp/tag/hvm5qw3c6od', - 'info_dict': { - 'id': 'hvm5qw3c6od', - 'title': 'アイマスMOIW2023ラジオ', - }, - 'playlist_mincount': 13, - }] - - def _real_extract(self, url): - tag_id = self._match_id(url) - webpage = self._download_webpage(url, tag_id) - title = traverse_obj(self._search_nextjs_data( - webpage, tag_id, fatal=False), ('props', 'pageProps', 'data', 'name', {str})) - - media = self._download_json( - f'https://channel.microcms.io/api/v1/media?limit=999&filters=(tag[contains]{tag_id})', - tag_id, headers=self._MICROCMS_HEADER) - - def entries(): - for metadata in traverse_obj(media, ('contents', lambda _, v: v['id'])): - yield { - '_type': 'url', - 'url': f'https://asobichannel.asobistore.jp/watch/{metadata["id"]}', - 'ie_key': AsobiChannelIE.ie_key(), - **self._extract_info(metadata), - } - - return self.playlist_result(entries(), tag_id, title) diff --git a/yt_dlp/extractor/asobistage.py b/yt_dlp/extractor/asobistage.py deleted file mode 100644 index 8fa8f3edb..000000000 --- a/yt_dlp/extractor/asobistage.py +++ /dev/null @@ -1,154 +0,0 @@ -import functools - -from .common import InfoExtractor -from ..utils import str_or_none, url_or_none -from ..utils.traversal import traverse_obj - - -class AsobiStageIE(InfoExtractor): - IE_DESC = 'ASOBISTAGE (アソビステージ)' - _VALID_URL = r'https?://asobistage\.asobistore\.jp/event/(?P(?P\w+)/(?Parchive|player)/(?P\w+))(?:[?#]|$)' - _TESTS = [{ - 'url': 'https://asobistage.asobistore.jp/event/315passionhour_2022summer/archive/frame', - 'info_dict': { - 'id': '315passionhour_2022summer/archive/frame', - 'title': '315プロダクションプレゼンツ 315パッションアワー!!!', - 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+', - }, - 'playlist_count': 1, - 'playlist': [{ - 'info_dict': { - 'id': 'edff52f2', - 'ext': 'mp4', - 'title': '315passion_FRAME_only', - 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+', - }, - }], - }, { - 'url': 'https://asobistage.asobistore.jp/event/idolmaster_idolworld2023_goods/archive/live', - 'info_dict': { - 'id': 'idolmaster_idolworld2023_goods/archive/live', - 'title': 'md5:378510b6e830129d505885908bd6c576', - 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+', - }, - 'playlist_count': 1, - 'playlist': [{ - 'info_dict': { - 'id': '3aef7110', - 'ext': 'mp4', - 'title': 'asobistore_station_1020_serverREC', - 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+', - }, - }], - }, { - 'url': 'https://asobistage.asobistore.jp/event/sidem_fclive_bpct/archive/premium_hc', - 'playlist_count': 4, - 'info_dict': { - 'id': 'sidem_fclive_bpct/archive/premium_hc', - 'title': '315 Production presents F@NTASTIC COMBINATION LIVE ~BRAINPOWER!!~/~CONNECTIME!!!!~', - 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+', - }, - }, { - 'url': 'https://asobistage.asobistore.jp/event/ijigenfes_utagassen/player/day1', - 'only_matching': True, - }] - - _API_HOST = 'https://asobistage-api.asobistore.jp' - _HEADERS = {} - _is_logged_in = False - - @functools.cached_property - def _owned_tickets(self): - owned_tickets = set() - if not self._is_logged_in: - return owned_tickets - - for path, name in [ - ('api/v1/purchase_history/list', 'ticket purchase history'), - ('api/v1/serialcode/list', 'redemption history'), - ]: - response = self._download_json( - f'{self._API_HOST}/{path}', None, f'Downloading {name}', - f'Unable to download {name}', expected_status=400) - if traverse_obj(response, ('payload', 'error_message'), 'error') == 'notlogin': - self._is_logged_in = False - break - owned_tickets.update( - traverse_obj(response, ('payload', 'value', ..., 'digital_product_id', {str_or_none}))) - - return owned_tickets - - def _get_available_channel_id(self, channel): - channel_id = traverse_obj(channel, ('chennel_vspf_id', {str})) - if not channel_id: - return None - # if rights_type_id == 6, then 'No conditions (no login required - non-members are OK)' - if traverse_obj(channel, ('viewrights', lambda _, v: v['rights_type_id'] == 6)): - return channel_id - available_tickets = traverse_obj(channel, ( - 'viewrights', ..., ('tickets', 'serialcodes'), ..., 'digital_product_id', {str_or_none})) - if not self._owned_tickets.intersection(available_tickets): - self.report_warning( - f'You are not a ticketholder for "{channel.get("channel_name") or channel_id}"') - return None - return channel_id - - def _real_initialize(self): - if self._get_cookies(self._API_HOST): - self._is_logged_in = True - token = self._download_json( - f'{self._API_HOST}/api/v1/vspf/token', None, 'Getting token', 'Unable to get token') - self._HEADERS['Authorization'] = f'Bearer {token}' - - def _real_extract(self, url): - video_id, event, type_, slug = self._match_valid_url(url).group('id', 'event', 'type', 'slug') - video_type = {'archive': 'archives', 'player': 'broadcasts'}[type_] - webpage = self._download_webpage(url, video_id) - event_data = traverse_obj( - self._search_nextjs_data(webpage, video_id, default={}), - ('props', 'pageProps', 'eventCMSData', { - 'title': ('event_name', {str}), - 'thumbnail': ('event_thumbnail_image', {url_or_none}), - })) - - available_channels = traverse_obj(self._download_json( - f'https://asobistage.asobistore.jp/cdn/v101/events/{event}/{video_type}.json', - video_id, 'Getting channel list', 'Unable to get channel list'), ( - video_type, lambda _, v: v['broadcast_slug'] == slug, - 'channels', lambda _, v: v['chennel_vspf_id'] != '00000')) - - entries = [] - for channel_id in traverse_obj(available_channels, (..., {self._get_available_channel_id})): - if video_type == 'archives': - channel_json = self._download_json( - f'https://survapi.channel.or.jp/proxy/v1/contents/{channel_id}/get_by_cuid', channel_id, - 'Getting archive channel info', 'Unable to get archive channel info', fatal=False, - headers=self._HEADERS) - channel_data = traverse_obj(channel_json, ('ex_content', { - 'm3u8_url': 'streaming_url', - 'title': 'title', - 'thumbnail': ('thumbnail', 'url'), - })) - else: # video_type == 'broadcasts' - channel_json = self._download_json( - f'https://survapi.channel.or.jp/ex/events/{channel_id}', channel_id, - 'Getting live channel info', 'Unable to get live channel info', fatal=False, - headers=self._HEADERS, query={'embed': 'channel'}) - channel_data = traverse_obj(channel_json, ('data', { - 'm3u8_url': ('Channel', 'Custom_live_url'), - 'title': 'Name', - 'thumbnail': 'Poster_url', - })) - - entries.append({ - 'id': channel_id, - 'title': channel_data.get('title'), - 'formats': self._extract_m3u8_formats(channel_data.get('m3u8_url'), channel_id, fatal=False), - 'is_live': video_type == 'broadcasts', - 'thumbnail': url_or_none(channel_data.get('thumbnail')), - }) - - if not self._is_logged_in and not entries: - self.raise_login_required() - - return self.playlist_result(entries, video_id, **event_data)