asobistage.py 6.8 KB

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