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