picarto.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import urllib.parse
  2. from .common import InfoExtractor
  3. from ..utils import (
  4. ExtractorError,
  5. str_or_none,
  6. traverse_obj,
  7. )
  8. class PicartoIE(InfoExtractor):
  9. _VALID_URL = r'https?://(?:www.)?picarto\.tv/(?P<id>[a-zA-Z0-9]+)'
  10. _TEST = {
  11. 'url': 'https://picarto.tv/Setz',
  12. 'info_dict': {
  13. 'id': 'Setz',
  14. 'ext': 'mp4',
  15. 'title': 're:^Setz [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
  16. 'timestamp': int,
  17. 'is_live': True,
  18. },
  19. 'skip': 'Stream is offline',
  20. }
  21. @classmethod
  22. def suitable(cls, url):
  23. return False if PicartoVodIE.suitable(url) else super().suitable(url)
  24. def _real_extract(self, url):
  25. channel_id = self._match_id(url)
  26. data = self._download_json(
  27. 'https://ptvintern.picarto.tv/ptvapi', channel_id, query={
  28. 'query': '''{
  29. channel(name: "%s") {
  30. adult
  31. id
  32. online
  33. stream_name
  34. title
  35. }
  36. getLoadBalancerUrl(channel_name: "%s") {
  37. url
  38. }
  39. }''' % (channel_id, channel_id), # noqa: UP031
  40. })['data']
  41. metadata = data['channel']
  42. if metadata.get('online') == 0:
  43. raise ExtractorError('Stream is offline', expected=True)
  44. title = metadata['title']
  45. cdn_data = self._download_json(
  46. data['getLoadBalancerUrl']['url'] + '/stream/json_' + metadata['stream_name'] + '.js',
  47. channel_id, 'Downloading load balancing info')
  48. formats = []
  49. for source in (cdn_data.get('source') or []):
  50. source_url = source.get('url')
  51. if not source_url:
  52. continue
  53. source_type = source.get('type')
  54. if source_type == 'html5/application/vnd.apple.mpegurl':
  55. formats.extend(self._extract_m3u8_formats(
  56. source_url, channel_id, 'mp4', m3u8_id='hls', fatal=False))
  57. elif source_type == 'html5/video/mp4':
  58. formats.append({
  59. 'url': source_url,
  60. })
  61. mature = metadata.get('adult')
  62. if mature is None:
  63. age_limit = None
  64. else:
  65. age_limit = 18 if mature is True else 0
  66. return {
  67. 'id': channel_id,
  68. 'title': title.strip(),
  69. 'is_live': True,
  70. 'channel': channel_id,
  71. 'channel_id': metadata.get('id'),
  72. 'channel_url': f'https://picarto.tv/{channel_id}',
  73. 'age_limit': age_limit,
  74. 'formats': formats,
  75. }
  76. class PicartoVodIE(InfoExtractor):
  77. _VALID_URL = r'https?://(?:www\.)?picarto\.tv/(?:videopopout|\w+/videos)/(?P<id>[^/?#&]+)'
  78. _TESTS = [{
  79. 'url': 'https://picarto.tv/videopopout/ArtofZod_2017.12.12.00.13.23.flv',
  80. 'md5': '3ab45ba4352c52ee841a28fb73f2d9ca',
  81. 'info_dict': {
  82. 'id': 'ArtofZod_2017.12.12.00.13.23.flv',
  83. 'ext': 'mp4',
  84. 'title': 'ArtofZod_2017.12.12.00.13.23.flv',
  85. 'thumbnail': r're:^https?://.*\.jpg',
  86. },
  87. 'skip': 'The VOD does not exist',
  88. }, {
  89. 'url': 'https://picarto.tv/ArtofZod/videos/772650',
  90. 'md5': '00067a0889f1f6869cc512e3e79c521b',
  91. 'info_dict': {
  92. 'id': '772650',
  93. 'ext': 'mp4',
  94. 'title': 'Art of Zod - Drawing and Painting',
  95. 'thumbnail': r're:^https?://.*\.jpg',
  96. 'channel': 'ArtofZod',
  97. 'age_limit': 18,
  98. },
  99. }, {
  100. 'url': 'https://picarto.tv/videopopout/Plague',
  101. 'only_matching': True,
  102. }]
  103. def _real_extract(self, url):
  104. video_id = self._match_id(url)
  105. data = self._download_json(
  106. 'https://ptvintern.picarto.tv/ptvapi', video_id, query={
  107. 'query': f'''{{
  108. video(id: "{video_id}") {{
  109. id
  110. title
  111. adult
  112. file_name
  113. video_recording_image_url
  114. channel {{
  115. name
  116. }}
  117. }}
  118. }}''',
  119. })['data']['video']
  120. file_name = data['file_name']
  121. netloc = urllib.parse.urlparse(data['video_recording_image_url']).netloc
  122. formats = self._extract_m3u8_formats(
  123. f'https://{netloc}/stream/hls/{file_name}/index.m3u8', video_id, 'mp4', m3u8_id='hls')
  124. return {
  125. 'id': video_id,
  126. **traverse_obj(data, {
  127. 'id': ('id', {str_or_none}),
  128. 'title': ('title', {str}),
  129. 'thumbnail': 'video_recording_image_url',
  130. 'channel': ('channel', 'name', {str}),
  131. 'age_limit': ('adult', {lambda x: 18 if x else 0}),
  132. }),
  133. 'formats': formats,
  134. }