zaiko.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import base64
  2. from .common import InfoExtractor
  3. from ..utils import (
  4. ExtractorError,
  5. extract_attributes,
  6. int_or_none,
  7. str_or_none,
  8. traverse_obj,
  9. try_call,
  10. unescapeHTML,
  11. url_basename,
  12. url_or_none,
  13. )
  14. class ZaikoBaseIE(InfoExtractor):
  15. def _download_real_webpage(self, url, video_id):
  16. webpage, urlh = self._download_webpage_handle(url, video_id)
  17. final_url = urlh.url
  18. if 'zaiko.io/login' in final_url:
  19. self.raise_login_required()
  20. elif '/_buy/' in final_url:
  21. raise ExtractorError('Your account does not have tickets to this event', expected=True)
  22. return webpage
  23. def _parse_vue_element_attr(self, name, string, video_id):
  24. page_elem = self._search_regex(rf'(<{name}[^>]+>)', string, name)
  25. attrs = {}
  26. for key, value in extract_attributes(page_elem).items():
  27. if key.startswith(':'):
  28. attrs[key[1:]] = self._parse_json(
  29. value, video_id, transform_source=unescapeHTML, fatal=False)
  30. return attrs
  31. class ZaikoIE(ZaikoBaseIE):
  32. _VALID_URL = r'https?://(?:[\w-]+\.)?zaiko\.io/event/(?P<id>\d+)/stream(?:/\d+)+'
  33. _TESTS = [{
  34. 'url': 'https://zaiko.io/event/324868/stream/20571/20571',
  35. 'info_dict': {
  36. 'id': '324868',
  37. 'ext': 'mp4',
  38. 'title': 'ZAIKO STREAMING TEST',
  39. 'alt_title': '[VOD] ZAIKO STREAMING TEST_20210603(Do Not Delete)',
  40. 'uploader_id': '454',
  41. 'uploader': 'ZAIKO ZERO',
  42. 'release_timestamp': 1583809200,
  43. 'thumbnail': r're:^https://[\w.-]+/\w+/\w+',
  44. 'thumbnails': 'maxcount:2',
  45. 'release_date': '20200310',
  46. 'categories': ['Tech House'],
  47. 'live_status': 'was_live',
  48. },
  49. 'params': {'skip_download': 'm3u8'},
  50. 'skip': 'Your account does not have tickets to this event',
  51. }]
  52. def _real_extract(self, url):
  53. video_id = self._match_id(url)
  54. webpage = self._download_real_webpage(url, video_id)
  55. stream_meta = self._parse_vue_element_attr('stream-page', webpage, video_id)
  56. player_page = self._download_webpage(
  57. stream_meta['stream-access']['video_source'], video_id,
  58. 'Downloading player page', headers={'referer': 'https://zaiko.io/'})
  59. player_meta = self._parse_vue_element_attr('player', player_page, video_id)
  60. status = traverse_obj(player_meta, ('initial_event_info', 'status', {str}))
  61. live_status, msg, expected = {
  62. 'vod': ('was_live', 'No VOD stream URL was found', False),
  63. 'archiving': ('post_live', 'Event VOD is still being processed', True),
  64. 'deleting': ('post_live', 'This event has ended', True),
  65. 'deleted': ('post_live', 'This event has ended', True),
  66. 'error': ('post_live', 'This event has ended', True),
  67. 'disconnected': ('post_live', 'Stream has been disconnected', True),
  68. 'live_to_disconnected': ('post_live', 'Stream has been disconnected', True),
  69. 'live': ('is_live', 'No livestream URL found was found', False),
  70. 'waiting': ('is_upcoming', 'Live event has not yet started', True),
  71. 'cancelled': ('not_live', 'Event has been cancelled', True),
  72. }.get(status) or ('not_live', f'Unknown event status "{status}"', False)
  73. stream_url = traverse_obj(player_meta, ('initial_event_info', 'endpoint', {url_or_none}))
  74. formats = self._extract_m3u8_formats(
  75. stream_url, video_id, live=True, fatal=False) if stream_url else []
  76. if not formats:
  77. self.raise_no_formats(msg, expected=expected)
  78. thumbnail_urls = [
  79. traverse_obj(player_meta, ('initial_event_info', 'poster_url')),
  80. self._og_search_thumbnail(self._download_webpage(
  81. f'https://zaiko.io/event/{video_id}', video_id, 'Downloading event page', fatal=False) or ''),
  82. ]
  83. return {
  84. 'id': video_id,
  85. 'formats': formats,
  86. 'live_status': live_status,
  87. **traverse_obj(stream_meta, {
  88. 'title': ('event', 'name', {str}),
  89. 'uploader': ('profile', 'name', {str}),
  90. 'uploader_id': ('profile', 'id', {str_or_none}),
  91. 'release_timestamp': ('stream', 'start', 'timestamp', {int_or_none}),
  92. 'categories': ('event', 'genres', ..., {lambda x: x or None}),
  93. }),
  94. **traverse_obj(player_meta, ('initial_event_info', {
  95. 'alt_title': ('title', {str}),
  96. })),
  97. 'thumbnails': [{'url': url, 'id': url_basename(url)} for url in thumbnail_urls if url_or_none(url)],
  98. }
  99. class ZaikoETicketIE(ZaikoBaseIE):
  100. _VALID_URL = r'https?://(?:www.)?zaiko\.io/account/eticket/(?P<id>[\w=-]{49})'
  101. _TESTS = [{
  102. 'url': 'https://zaiko.io/account/eticket/TZjMwMzQ2Y2EzMXwyMDIzMDYwNzEyMTMyNXw1MDViOWU2Mw==',
  103. 'playlist_count': 1,
  104. 'info_dict': {
  105. 'id': 'f30346ca31-20230607121325-505b9e63',
  106. 'title': 'ZAIKO STREAMING TEST',
  107. 'thumbnail': 'https://media.zkocdn.net/pf_1/1_3wdyjcjyupseatkwid34u',
  108. },
  109. 'skip': 'Only available with the ticketholding account',
  110. }]
  111. def _real_extract(self, url):
  112. ticket_id = self._match_id(url)
  113. ticket_id = try_call(
  114. lambda: base64.urlsafe_b64decode(ticket_id[1:]).decode().replace('|', '-')) or ticket_id
  115. webpage = self._download_real_webpage(url, ticket_id)
  116. eticket = self._parse_vue_element_attr('eticket', webpage, ticket_id)
  117. return self.playlist_result(
  118. [self.url_result(stream, ZaikoIE) for stream in traverse_obj(eticket, ('streams', ..., 'url'))],
  119. ticket_id, **traverse_obj(eticket, ('ticket-details', {
  120. 'title': 'event_name',
  121. 'thumbnail': 'event_img_url',
  122. })))