kukululive.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import urllib.parse
  2. from .common import InfoExtractor
  3. from ..utils import (
  4. ExtractorError,
  5. clean_html,
  6. filter_dict,
  7. get_element_by_id,
  8. int_or_none,
  9. join_nonempty,
  10. js_to_json,
  11. qualities,
  12. url_or_none,
  13. urljoin,
  14. )
  15. from ..utils.traversal import traverse_obj
  16. class KukuluLiveIE(InfoExtractor):
  17. _VALID_URL = r'https?://live\.erinn\.biz/live\.php\?h(?P<id>\d+)'
  18. _TESTS = [{
  19. 'url': 'https://live.erinn.biz/live.php?h675134569',
  20. 'md5': 'e380fa6a47fc703d91cea913ab44ec2e',
  21. 'info_dict': {
  22. 'id': '675134569',
  23. 'ext': 'mp4',
  24. 'title': 'プロセカ',
  25. 'description': 'テストも兼ねたプロセカ配信。',
  26. 'timestamp': 1702689148,
  27. 'upload_date': '20231216',
  28. 'thumbnail': r're:^https?://.*',
  29. },
  30. }, {
  31. 'url': 'https://live.erinn.biz/live.php?h102338092',
  32. 'md5': 'dcf5167a934b1c60333461e13a81a6e2',
  33. 'info_dict': {
  34. 'id': '102338092',
  35. 'ext': 'mp4',
  36. 'title': 'Among Usで遊びます!!',
  37. 'description': 'VTuberになりましたねんねこ㌨ですよろしくお願いします',
  38. 'timestamp': 1704603118,
  39. 'upload_date': '20240107',
  40. 'thumbnail': r're:^https?://.*',
  41. },
  42. }, {
  43. 'url': 'https://live.erinn.biz/live.php?h878049531',
  44. 'only_matching': True,
  45. }]
  46. def _get_quality_meta(self, video_id, desc, code, force_h264=None):
  47. desc += ' (force_h264)' if force_h264 else ''
  48. qs = self._download_webpage(
  49. 'https://live.erinn.biz/live.player.fplayer.php', video_id,
  50. f'Downloading {desc} quality metadata', f'Unable to download {desc} quality metadata',
  51. query=filter_dict({
  52. 'hash': video_id,
  53. 'action': f'get{code}liveByAjax',
  54. 'force_h264': force_h264,
  55. }))
  56. return urllib.parse.parse_qs(qs)
  57. def _add_quality_formats(self, formats, quality_meta):
  58. vcodec = traverse_obj(quality_meta, ('vcodec', 0, {str}))
  59. quality = traverse_obj(quality_meta, ('now_quality', 0, {str}))
  60. quality_priority = qualities(('low', 'h264', 'high'))(quality)
  61. if traverse_obj(quality_meta, ('hlsaddr', 0, {url_or_none})):
  62. formats.append({
  63. 'format_id': quality,
  64. 'url': quality_meta['hlsaddr'][0],
  65. 'ext': 'mp4',
  66. 'vcodec': vcodec,
  67. 'quality': quality_priority,
  68. })
  69. if traverse_obj(quality_meta, ('hlsaddr_audioonly', 0, {url_or_none})):
  70. formats.append({
  71. 'format_id': join_nonempty(quality, 'audioonly'),
  72. 'url': quality_meta['hlsaddr_audioonly'][0],
  73. 'ext': 'm4a',
  74. 'vcodec': 'none',
  75. 'quality': quality_priority,
  76. })
  77. def _real_extract(self, url):
  78. video_id = self._match_id(url)
  79. html = self._download_webpage(url, video_id)
  80. if '>タイムシフトが見つかりませんでした。<' in html:
  81. raise ExtractorError('This stream has expired', expected=True)
  82. title = clean_html(
  83. get_element_by_id('livetitle', html.replace('<SPAN', '<span').replace('SPAN>', 'span>')))
  84. description = self._html_search_meta('Description', html)
  85. thumbnail = self._html_search_meta(['og:image', 'twitter:image'], html)
  86. if self._search_regex(r'(var\s+timeshift\s*=\s*false)', html, 'is livestream', default=False):
  87. formats = []
  88. for (desc, code) in [('high', 'Z'), ('low', 'ForceLow')]:
  89. quality_meta = self._get_quality_meta(video_id, desc, code)
  90. self._add_quality_formats(formats, quality_meta)
  91. if desc == 'high' and traverse_obj(quality_meta, ('vcodec', 0)) == 'HEVC':
  92. self._add_quality_formats(
  93. formats, self._get_quality_meta(video_id, desc, code, force_h264='1'))
  94. return {
  95. 'id': video_id,
  96. 'title': title,
  97. 'description': description,
  98. 'thumbnail': thumbnail,
  99. 'is_live': True,
  100. 'formats': formats,
  101. }
  102. # VOD extraction
  103. player_html = self._download_webpage(
  104. 'https://live.erinn.biz/live.timeshift.fplayer.php', video_id,
  105. 'Downloading player html', 'Unable to download player html', query={'hash': video_id})
  106. sources = traverse_obj(self._search_json(
  107. r'var\s+fplayer_source\s*=', player_html, 'stream data', video_id,
  108. contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json), lambda _, v: v['file'])
  109. def entries(segments, playlist=True):
  110. for i, segment in enumerate(segments, 1):
  111. yield {
  112. 'id': f'{video_id}_{i}' if playlist else video_id,
  113. 'title': f'{title} (Part {i})' if playlist else title,
  114. 'description': description,
  115. 'timestamp': traverse_obj(segment, ('time_start', {int_or_none})),
  116. 'thumbnail': thumbnail,
  117. 'formats': [{
  118. 'url': urljoin('https://live.erinn.biz', segment['file']),
  119. 'ext': 'mp4',
  120. 'protocol': 'm3u8_native',
  121. }],
  122. }
  123. if len(sources) == 1:
  124. return next(entries(sources, playlist=False))
  125. return self.playlist_result(entries(sources), video_id, title, description, multi_video=True)