]>
Commit | Line | Data |
---|---|---|
1 | import functools | |
2 | ||
3 | from .common import InfoExtractor | |
4 | from ..utils import str_or_none, url_or_none | |
5 | from ..utils.traversal import traverse_obj | |
6 | ||
7 | ||
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+', | |
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( | |
108 | self._search_nextjs_data(webpage, video_id, default={}), | |
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) |