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