]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/asobistage.py
[ie] Make `_search_nextjs_data` non fatal (#8937)
[yt-dlp.git] / yt_dlp / extractor / asobistage.py
CommitLineData
0284f1fe
M
1import functools
2
3from .common import InfoExtractor
4from ..utils import str_or_none, url_or_none
5from ..utils.traversal import traverse_obj
6
7
8class 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+',
17 },
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+',
25 },
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+',
33 },
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+',
41 },
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+',
50 },
51 }, {
52 'url': 'https://asobistage.asobistore.jp/event/ijigenfes_utagassen/player/day1',
53 'only_matching': True,
54 }]
55
56 _API_HOST = 'https://asobistage-api.asobistore.jp'
57 _HEADERS = {}
58 _is_logged_in = False
59
60 @functools.cached_property
61 def _owned_tickets(self):
62 owned_tickets = set()
63 if not self._is_logged_in:
64 return owned_tickets
65
66 for path, name in [
67 ('api/v1/purchase_history/list', 'ticket purchase history'),
68 ('api/v1/serialcode/list', 'redemption history'),
69 ]:
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})))
78
79 return owned_tickets
80
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
95
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}'
102
103 def _real_extract(self, url):
104 video_id, event, type_, slug = self._match_valid_url(url).group('id', 'event', 'type', 'slug')
105 video_type = {'archive': 'archives', 'player': 'broadcasts'}[type_]
106 webpage = self._download_webpage(url, video_id)
107 event_data = traverse_obj(
3ee11942 108 self._search_nextjs_data(webpage, video_id, default={}),
0284f1fe
M
109 ('props', 'pageProps', 'eventCMSData', {
110 'title': ('event_name', {str}),
111 'thumbnail': ('event_thumbnail_image', {url_or_none}),
112 }))
113
114 available_channels = traverse_obj(self._download_json(
115 f'https://asobistage.asobistore.jp/cdn/v101/events/{event}/{video_type}.json',
116 video_id, 'Getting channel list', 'Unable to get channel list'), (
117 video_type, lambda _, v: v['broadcast_slug'] == slug,
118 'channels', lambda _, v: v['chennel_vspf_id'] != '00000'))
119
120 entries = []
121 for channel_id in traverse_obj(available_channels, (..., {self._get_available_channel_id})):
122 if video_type == 'archives':
123 channel_json = self._download_json(
124 f'https://survapi.channel.or.jp/proxy/v1/contents/{channel_id}/get_by_cuid', channel_id,
125 'Getting archive channel info', 'Unable to get archive channel info', fatal=False,
126 headers=self._HEADERS)
127 channel_data = traverse_obj(channel_json, ('ex_content', {
128 'm3u8_url': 'streaming_url',
129 'title': 'title',
130 'thumbnail': ('thumbnail', 'url'),
131 }))
132 else: # video_type == 'broadcasts'
133 channel_json = self._download_json(
134 f'https://survapi.channel.or.jp/ex/events/{channel_id}', channel_id,
135 'Getting live channel info', 'Unable to get live channel info', fatal=False,
136 headers=self._HEADERS, query={'embed': 'channel'})
137 channel_data = traverse_obj(channel_json, ('data', {
138 'm3u8_url': ('Channel', 'Custom_live_url'),
139 'title': 'Name',
140 'thumbnail': 'Poster_url',
141 }))
142
143 entries.append({
144 'id': channel_id,
145 'title': channel_data.get('title'),
146 'formats': self._extract_m3u8_formats(channel_data.get('m3u8_url'), channel_id, fatal=False),
147 'is_live': video_type == 'broadcasts',
148 'thumbnail': url_or_none(channel_data.get('thumbnail')),
149 })
150
151 if not self._is_logged_in and not entries:
152 self.raise_login_required()
153
154 return self.playlist_result(entries, video_id, **event_data)