[cleanup] Make more playlist entries lazy (#11763)
[yt-dlp.git] / yt_dlp / extractor / asobistage.py
blob0437908bff2fd8718122b695e795d7e956ede728
1 import functools
3 from .common import InfoExtractor
4 from ..utils import str_or_none, url_or_none
5 from ..utils.traversal import traverse_obj
8 class AsobiStageIE(InfoExtractor):
9 IE_DESC = 'ASOBISTAGE (アソビステージ)'
10 _VALID_URL = r'https?://asobistage\.asobistore\.jp/event/(?P<id>(?P<event>\w+)/(?P<type>archive|player)/(?P<slug>\w+))(?:[?#]|$)'
11 _TESTS = [{
12 'url': 'https://asobistage.asobistore.jp/event/315passionhour_2022summer/archive/frame',
13 'info_dict': {
14 'id': '315passionhour_2022summer/archive/frame',
15 'title': '315プロダクションプレゼンツ 315パッションアワー!!!',
16 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+',
18 'playlist_count': 1,
19 'playlist': [{
20 'info_dict': {
21 'id': 'edff52f2',
22 'ext': 'mp4',
23 'title': '315passion_FRAME_only',
24 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+',
26 }],
27 }, {
28 'url': 'https://asobistage.asobistore.jp/event/idolmaster_idolworld2023_goods/archive/live',
29 'info_dict': {
30 'id': 'idolmaster_idolworld2023_goods/archive/live',
31 'title': 'md5:378510b6e830129d505885908bd6c576',
32 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+',
34 'playlist_count': 1,
35 'playlist': [{
36 'info_dict': {
37 'id': '3aef7110',
38 'ext': 'mp4',
39 'title': 'asobistore_station_1020_serverREC',
40 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+',
42 }],
43 }, {
44 'url': 'https://asobistage.asobistore.jp/event/sidem_fclive_bpct/archive/premium_hc',
45 'playlist_count': 4,
46 'info_dict': {
47 'id': 'sidem_fclive_bpct/archive/premium_hc',
48 'title': '315 Production presents F@NTASTIC COMBINATION LIVE ~BRAINPOWER!!~/~CONNECTIME!!!!~',
49 'thumbnail': r're:^https?://[\w.-]+/\w+/\w+',
51 }, {
52 'url': 'https://asobistage.asobistore.jp/event/ijigenfes_utagassen/player/day1',
53 'only_matching': True,
56 _API_HOST = 'https://asobistage-api.asobistore.jp'
57 _HEADERS = {}
58 _is_logged_in = False
60 @functools.cached_property
61 def _owned_tickets(self):
62 owned_tickets = set()
63 if not self._is_logged_in:
64 return owned_tickets
66 for path, name in [
67 ('api/v1/purchase_history/list', 'ticket purchase history'),
68 ('api/v1/serialcode/list', 'redemption history'),
70 response = self._download_json(
71 f'{self._API_HOST}/{path}', None, f'Downloading {name}',
72 f'Unable to download {name}', expected_status=400)
73 if traverse_obj(response, ('payload', 'error_message'), 'error') == 'notlogin':
74 self._is_logged_in = False
75 break
76 owned_tickets.update(
77 traverse_obj(response, ('payload', 'value', ..., 'digital_product_id', {str_or_none})))
79 return owned_tickets
81 def _get_available_channel_id(self, channel):
82 channel_id = traverse_obj(channel, ('chennel_vspf_id', {str}))
83 if not channel_id:
84 return None
85 # if rights_type_id == 6, then 'No conditions (no login required - non-members are OK)'
86 if traverse_obj(channel, ('viewrights', lambda _, v: v['rights_type_id'] == 6)):
87 return channel_id
88 available_tickets = traverse_obj(channel, (
89 'viewrights', ..., ('tickets', 'serialcodes'), ..., 'digital_product_id', {str_or_none}))
90 if not self._owned_tickets.intersection(available_tickets):
91 self.report_warning(
92 f'You are not a ticketholder for "{channel.get("channel_name") or channel_id}"')
93 return None
94 return channel_id
96 def _real_initialize(self):
97 if self._get_cookies(self._API_HOST):
98 self._is_logged_in = True
99 token = self._download_json(
100 f'{self._API_HOST}/api/v1/vspf/token', None, 'Getting token', 'Unable to get token')
101 self._HEADERS['Authorization'] = f'Bearer {token}'
103 def _real_extract(self, url):
104 webpage, urlh = self._download_webpage_handle(url, self._match_id(url))
105 video_id, event, type_, slug = self._match_valid_url(urlh.url).group('id', 'event', 'type', 'slug')
106 video_type = {'archive': 'archives', 'player': 'broadcasts'}[type_]
108 event_data = traverse_obj(
109 self._search_nextjs_data(webpage, video_id, default={}),
110 ('props', 'pageProps', 'eventCMSData', {
111 'title': ('event_name', {str}),
112 'thumbnail': ('event_thumbnail_image', {url_or_none}),
115 available_channels = traverse_obj(self._download_json(
116 f'https://asobistage.asobistore.jp/cdn/v101/events/{event}/{video_type}.json',
117 video_id, 'Getting channel list', 'Unable to get channel list'), (
118 video_type, lambda _, v: v['broadcast_slug'] == slug,
119 'channels', lambda _, v: v['chennel_vspf_id'] != '00000'))
121 entries = []
122 for channel_id in traverse_obj(available_channels, (..., {self._get_available_channel_id})):
123 if video_type == 'archives':
124 channel_json = self._download_json(
125 f'https://survapi.channel.or.jp/proxy/v1/contents/{channel_id}/get_by_cuid', channel_id,
126 'Getting archive channel info', 'Unable to get archive channel info', fatal=False,
127 headers=self._HEADERS)
128 channel_data = traverse_obj(channel_json, ('ex_content', {
129 'm3u8_url': 'streaming_url',
130 'title': 'title',
131 'thumbnail': ('thumbnail', 'url'),
133 else: # video_type == 'broadcasts'
134 channel_json = self._download_json(
135 f'https://survapi.channel.or.jp/ex/events/{channel_id}', channel_id,
136 'Getting live channel info', 'Unable to get live channel info', fatal=False,
137 headers=self._HEADERS, query={'embed': 'channel'})
138 channel_data = traverse_obj(channel_json, ('data', {
139 'm3u8_url': ('Channel', 'Custom_live_url'),
140 'title': 'Name',
141 'thumbnail': 'Poster_url',
144 entries.append({
145 'id': channel_id,
146 'title': channel_data.get('title'),
147 'formats': self._extract_m3u8_formats(channel_data.get('m3u8_url'), channel_id, fatal=False),
148 'is_live': video_type == 'broadcasts',
149 'thumbnail': url_or_none(channel_data.get('thumbnail')),
152 if not self._is_logged_in and not entries:
153 self.raise_login_required()
155 return self.playlist_result(entries, video_id, **event_data)