]>
Commit | Line | Data |
---|---|---|
92aa6d68 | 1 | import itertools |
2 | import json | |
33b737be | 3 | import re |
92aa6d68 | 4 | |
5 | from .common import InfoExtractor | |
3d2623a8 | 6 | from ..networking import HEADRequest |
92aa6d68 | 7 | from ..utils import ( |
d2c8aadf | 8 | ExtractorError, |
33b737be | 9 | UnsupportedError, |
10 | determine_ext, | |
92aa6d68 | 11 | int_or_none, |
33b737be | 12 | parse_resolution, |
92aa6d68 | 13 | str_or_none, |
14 | traverse_obj, | |
92aa6d68 | 15 | unified_timestamp, |
16 | url_basename, | |
33b737be | 17 | urljoin, |
18 | url_or_none, | |
92aa6d68 | 19 | ) |
20 | ||
21 | ||
22 | class TrillerBaseIE(InfoExtractor): | |
23 | _NETRC_MACHINE = 'triller' | |
92aa6d68 | 24 | _API_BASE_URL = 'https://social.triller.co/v1.5' |
d6f88719 | 25 | _API_HEADERS = {'Origin': 'https://triller.co'} |
92aa6d68 | 26 | |
27 | def _perform_login(self, username, password): | |
d6f88719 | 28 | if self._API_HEADERS.get('Authorization'): |
92aa6d68 | 29 | return |
30 | ||
33b737be | 31 | headers = {**self._API_HEADERS, 'Content-Type': 'application/json'} |
32 | user_check = traverse_obj(self._download_json( | |
92aa6d68 | 33 | f'{self._API_BASE_URL}/api/user/is-valid-username', None, note='Checking username', |
33b737be | 34 | fatal=False, expected_status=400, headers=headers, |
35 | data=json.dumps({'username': username}, separators=(',', ':')).encode()), 'status') | |
36 | ||
37 | if user_check: # endpoint returns `"status":false` if username exists | |
92aa6d68 | 38 | raise ExtractorError('Unable to login: Invalid username', expected=True) |
39 | ||
92aa6d68 | 40 | login = self._download_json( |
33b737be | 41 | f'{self._API_BASE_URL}/user/auth', None, note='Logging in', fatal=False, |
42 | expected_status=400, headers=headers, data=json.dumps({ | |
43 | 'username': username, | |
44 | 'password': password, | |
45 | }, separators=(',', ':')).encode()) or {} | |
46 | ||
92aa6d68 | 47 | if not login.get('auth_token'): |
48 | if login.get('error') == 1008: | |
49 | raise ExtractorError('Unable to login: Incorrect password', expected=True) | |
50 | raise ExtractorError('Unable to login') | |
51 | ||
d6f88719 | 52 | self._API_HEADERS['Authorization'] = f'Bearer {login["auth_token"]}' |
92aa6d68 | 53 | |
54 | def _get_comments(self, video_id, limit=15): | |
55 | comment_info = self._download_json( | |
56 | f'{self._API_BASE_URL}/api/videos/{video_id}/comments_v2', | |
57 | video_id, fatal=False, note='Downloading comments API JSON', | |
d6f88719 | 58 | headers=self._API_HEADERS, query={'limit': limit}) or {} |
92aa6d68 | 59 | if not comment_info.get('comments'): |
60 | return | |
33b737be | 61 | yield from traverse_obj(comment_info, ('comments', ..., { |
62 | 'id': ('id', {str_or_none}), | |
63 | 'text': 'body', | |
64 | 'author': ('author', 'username'), | |
65 | 'author_id': ('author', 'user_id'), | |
66 | 'timestamp': ('timestamp', {unified_timestamp}), | |
67 | })) | |
92aa6d68 | 68 | |
33b737be | 69 | def _parse_video_info(self, video_info, username, user_id, display_id=None): |
70 | video_id = str(video_info['id']) | |
71 | display_id = display_id or video_info.get('video_uuid') | |
72 | ||
73 | if traverse_obj(video_info, ( | |
74 | None, ('transcoded_url', 'video_url', 'stream_url', 'audio_url'), | |
75 | {lambda x: re.search(r'/copyright/', x)}), get_all=False): | |
76 | self.raise_no_formats('This video has been removed due to licensing restrictions', expected=True) | |
77 | ||
78 | def format_info(url): | |
79 | return { | |
80 | 'url': url, | |
81 | 'ext': determine_ext(url), | |
82 | 'format_id': url_basename(url).split('.')[0], | |
83 | } | |
92aa6d68 | 84 | |
85 | formats = [] | |
33b737be | 86 | |
87 | if determine_ext(video_info.get('transcoded_url')) == 'm3u8': | |
88 | formats.extend(self._extract_m3u8_formats( | |
89 | video_info['transcoded_url'], video_id, 'mp4', m3u8_id='hls', fatal=False)) | |
90 | ||
91 | for video in traverse_obj(video_info, ('video_set', lambda _, v: url_or_none(v['url']))): | |
92aa6d68 | 92 | formats.append({ |
33b737be | 93 | **format_info(video['url']), |
94 | **parse_resolution(video.get('resolution')), | |
92aa6d68 | 95 | 'vcodec': video.get('codec'), |
96 | 'vbr': int_or_none(video.get('bitrate'), 1000), | |
92aa6d68 | 97 | }) |
33b737be | 98 | |
99 | video_url = traverse_obj(video_info, 'video_url', 'stream_url', expected_type=url_or_none) | |
100 | if video_url: | |
92aa6d68 | 101 | formats.append({ |
33b737be | 102 | **format_info(video_url), |
103 | 'vcodec': 'h264', | |
104 | **traverse_obj(video_info, { | |
105 | 'width': 'width', | |
106 | 'height': 'height', | |
107 | 'filesize': 'filesize', | |
108 | }, expected_type=int_or_none), | |
92aa6d68 | 109 | }) |
110 | ||
33b737be | 111 | audio_url = url_or_none(video_info.get('audio_url')) |
112 | if audio_url: | |
113 | formats.append(format_info(audio_url)) | |
92aa6d68 | 114 | |
33b737be | 115 | comment_count = traverse_obj(video_info, ('comment_count', {int_or_none})) |
92aa6d68 | 116 | |
117 | return { | |
33b737be | 118 | 'id': video_id, |
119 | 'display_id': display_id, | |
120 | 'uploader': username, | |
121 | 'uploader_id': user_id or traverse_obj(video_info, ('user', 'user_id', {str_or_none})), | |
122 | 'webpage_url': urljoin(f'https://triller.co/@{username}/video/', display_id), | |
92aa6d68 | 123 | 'uploader_url': f'https://triller.co/@{username}', |
124 | 'extractor_key': TrillerIE.ie_key(), | |
125 | 'extractor': TrillerIE.IE_NAME, | |
126 | 'formats': formats, | |
127 | 'comment_count': comment_count, | |
128 | '__post_extractor': self.extract_comments(video_id, comment_count), | |
33b737be | 129 | **traverse_obj(video_info, { |
130 | 'title': ('description', {lambda x: x.replace('\r\n', ' ')}), | |
131 | 'description': 'description', | |
132 | 'creator': ((('user'), ('users', lambda _, v: str(v['user_id']) == user_id)), 'name'), | |
133 | 'thumbnail': ('thumbnail_url', {url_or_none}), | |
134 | 'timestamp': ('timestamp', {unified_timestamp}), | |
135 | 'duration': ('duration', {int_or_none}), | |
136 | 'view_count': ('play_count', {int_or_none}), | |
137 | 'like_count': ('likes_count', {int_or_none}), | |
138 | 'artist': 'song_artist', | |
139 | 'track': 'song_title', | |
140 | }, get_all=False), | |
92aa6d68 | 141 | } |
142 | ||
143 | ||
144 | class TrillerIE(TrillerBaseIE): | |
145 | _VALID_URL = r'''(?x) | |
146 | https?://(?:www\.)?triller\.co/ | |
33b737be | 147 | @(?P<username>[\w.]+)/video/(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}) |
92aa6d68 | 148 | ''' |
149 | _TESTS = [{ | |
150 | 'url': 'https://triller.co/@theestallion/video/2358fcd7-3df2-4c77-84c8-1d091610a6cf', | |
151 | 'md5': '228662d783923b60d78395fedddc0a20', | |
152 | 'info_dict': { | |
153 | 'id': '71595734', | |
154 | 'ext': 'mp4', | |
155 | 'title': 'md5:9a2bf9435c5c4292678996a464669416', | |
156 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', | |
157 | 'description': 'md5:9a2bf9435c5c4292678996a464669416', | |
158 | 'uploader': 'theestallion', | |
159 | 'uploader_id': '18992236', | |
160 | 'creator': 'Megan Thee Stallion', | |
161 | 'timestamp': 1660598222, | |
162 | 'upload_date': '20220815', | |
163 | 'duration': 47, | |
92aa6d68 | 164 | 'view_count': int, |
165 | 'like_count': int, | |
166 | 'artist': 'Megan Thee Stallion', | |
167 | 'track': 'Her', | |
92aa6d68 | 168 | 'uploader_url': 'https://triller.co/@theestallion', |
169 | 'comment_count': int, | |
33b737be | 170 | }, |
171 | 'skip': 'This video has been removed due to licensing restrictions', | |
92aa6d68 | 172 | }, { |
173 | 'url': 'https://triller.co/@charlidamelio/video/46c6fcfa-aa9e-4503-a50c-68444f44cddc', | |
174 | 'md5': '874055f462af5b0699b9dbb527a505a0', | |
175 | 'info_dict': { | |
176 | 'id': '71621339', | |
177 | 'ext': 'mp4', | |
178 | 'title': 'md5:4c91ea82760fe0fffb71b8c3aa7295fc', | |
33b737be | 179 | 'display_id': '46c6fcfa-aa9e-4503-a50c-68444f44cddc', |
92aa6d68 | 180 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', |
181 | 'description': 'md5:4c91ea82760fe0fffb71b8c3aa7295fc', | |
182 | 'uploader': 'charlidamelio', | |
183 | 'uploader_id': '1875551', | |
184 | 'creator': 'charli damelio', | |
185 | 'timestamp': 1660773354, | |
186 | 'upload_date': '20220817', | |
187 | 'duration': 16, | |
92aa6d68 | 188 | 'view_count': int, |
189 | 'like_count': int, | |
190 | 'artist': 'Dixie', | |
191 | 'track': 'Someone to Blame', | |
92aa6d68 | 192 | 'uploader_url': 'https://triller.co/@charlidamelio', |
193 | 'comment_count': int, | |
33b737be | 194 | }, |
195 | }, { | |
196 | 'url': 'https://triller.co/@theestallion/video/07f35f38-1f51-48e2-8c5f-f7a8e829988f', | |
197 | 'md5': 'af7b3553e4b8bfca507636471ee2eb41', | |
198 | 'info_dict': { | |
199 | 'id': '71837829', | |
200 | 'ext': 'mp4', | |
201 | 'title': 'UNGRATEFUL VIDEO OUT NOW ๐๐พ๐๐พ๐๐พ ๐๐ link my bio #womeninhiphop', | |
202 | 'display_id': '07f35f38-1f51-48e2-8c5f-f7a8e829988f', | |
203 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', | |
204 | 'description': 'UNGRATEFUL VIDEO OUT NOW ๐๐พ๐๐พ๐๐พ ๐๐ link my bio\r\n #womeninhiphop', | |
205 | 'uploader': 'theestallion', | |
206 | 'uploader_id': '18992236', | |
207 | 'creator': 'Megan Thee Stallion', | |
208 | 'timestamp': 1662486178, | |
209 | 'upload_date': '20220906', | |
210 | 'duration': 30, | |
211 | 'view_count': int, | |
212 | 'like_count': int, | |
213 | 'artist': 'Unknown', | |
214 | 'track': 'Unknown', | |
215 | 'uploader_url': 'https://triller.co/@theestallion', | |
216 | 'comment_count': int, | |
217 | }, | |
92aa6d68 | 218 | }] |
219 | ||
220 | def _real_extract(self, url): | |
33b737be | 221 | username, display_id = self._match_valid_url(url).group('username', 'id') |
222 | ||
223 | video_info = self._download_json( | |
224 | f'{self._API_BASE_URL}/api/videos/{display_id}', display_id, | |
225 | headers=self._API_HEADERS)['videos'][0] | |
92aa6d68 | 226 | |
33b737be | 227 | return self._parse_video_info(video_info, username, None, display_id) |
92aa6d68 | 228 | |
229 | ||
230 | class TrillerUserIE(TrillerBaseIE): | |
33b737be | 231 | _VALID_URL = r'https?://(?:www\.)?triller\.co/@(?P<id>[\w.]+)/?(?:$|[#?])' |
92aa6d68 | 232 | _TESTS = [{ |
92aa6d68 | 233 | 'url': 'https://triller.co/@theestallion', |
33b737be | 234 | 'playlist_mincount': 12, |
92aa6d68 | 235 | 'info_dict': { |
236 | 'id': '18992236', | |
237 | 'title': 'theestallion', | |
238 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', | |
33b737be | 239 | }, |
92aa6d68 | 240 | }, { |
241 | 'url': 'https://triller.co/@charlidamelio', | |
33b737be | 242 | 'playlist_mincount': 150, |
92aa6d68 | 243 | 'info_dict': { |
244 | 'id': '1875551', | |
245 | 'title': 'charlidamelio', | |
246 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', | |
33b737be | 247 | }, |
92aa6d68 | 248 | }] |
249 | ||
250 | def _real_initialize(self): | |
d6f88719 | 251 | if not self._API_HEADERS.get('Authorization'): |
92aa6d68 | 252 | guest = self._download_json( |
33b737be | 253 | f'{self._API_BASE_URL}/user/create_guest', None, |
254 | note='Creating guest session', data=b'', headers=self._API_HEADERS, query={ | |
92aa6d68 | 255 | 'platform': 'Web', |
256 | 'app_version': '', | |
257 | }) | |
258 | if not guest.get('auth_token'): | |
259 | raise ExtractorError('Unable to fetch required auth token for user extraction') | |
260 | ||
d6f88719 | 261 | self._API_HEADERS['Authorization'] = f'Bearer {guest["auth_token"]}' |
92aa6d68 | 262 | |
33b737be | 263 | def _entries(self, username, user_id, limit=6): |
264 | query = {'limit': limit} | |
92aa6d68 | 265 | for page in itertools.count(1): |
33b737be | 266 | videos = self._download_json( |
267 | f'{self._API_BASE_URL}/api/users/{user_id}/videos', | |
268 | username, note=f'Downloading user video list page {page}', | |
269 | headers=self._API_HEADERS, query=query) | |
270 | ||
271 | for video in traverse_obj(videos, ('videos', ...)): | |
272 | yield self._parse_video_info(video, username, user_id) | |
273 | ||
274 | query['before_time'] = traverse_obj(videos, ('videos', -1, 'timestamp')) | |
92aa6d68 | 275 | if not query['before_time']: |
276 | break | |
277 | ||
92aa6d68 | 278 | def _real_extract(self, url): |
279 | username = self._match_id(url) | |
33b737be | 280 | |
39837ae3 | 281 | user_info = traverse_obj(self._download_json( |
92aa6d68 | 282 | f'{self._API_BASE_URL}/api/users/by_username/{username}', |
39837ae3 | 283 | username, note='Downloading user info', headers=self._API_HEADERS), ('user', {dict})) or {} |
284 | ||
285 | if user_info.get('private') and user_info.get('followed_by_me') not in (True, 'true'): | |
286 | raise ExtractorError('This user profile is private', expected=True) | |
287 | elif traverse_obj(user_info, (('blocked_by_user', 'blocking_user'), {bool}), get_all=False): | |
288 | raise ExtractorError('The author of the video is blocked', expected=True) | |
92aa6d68 | 289 | |
290 | user_id = str_or_none(user_info.get('user_id')) | |
33b737be | 291 | if not user_id: |
292 | raise ExtractorError('Unable to extract user ID') | |
92aa6d68 | 293 | |
294 | return self.playlist_result( | |
33b737be | 295 | self._entries(username, user_id), user_id, username, thumbnail=user_info.get('avatar_url')) |
296 | ||
297 | ||
298 | class TrillerShortIE(InfoExtractor): | |
299 | _VALID_URL = r'https?://v\.triller\.co/(?P<id>\w+)' | |
300 | _TESTS = [{ | |
301 | 'url': 'https://v.triller.co/WWZNWk', | |
302 | 'md5': '5eb8dc2c971bd8cd794ec9e8d5e9d101', | |
303 | 'info_dict': { | |
304 | 'id': '66210052', | |
305 | 'ext': 'mp4', | |
306 | 'title': 'md5:2dfc89d154cd91a4a18cd9582ba03e16', | |
307 | 'display_id': 'f4480e1f-fb4e-45b9-a44c-9e6c679ce7eb', | |
308 | 'thumbnail': r're:^https://uploads\.cdn\.triller\.co/.+\.jpg$', | |
309 | 'description': 'md5:2dfc89d154cd91a4a18cd9582ba03e16', | |
310 | 'uploader': 'statefairent', | |
311 | 'uploader_id': '487545193', | |
312 | 'creator': 'Officialย Summerย Fairย ofย LA', | |
313 | 'timestamp': 1629655457, | |
314 | 'upload_date': '20210822', | |
315 | 'duration': 19, | |
316 | 'view_count': int, | |
317 | 'like_count': int, | |
318 | 'artist': 'Unknown', | |
319 | 'track': 'Unknown', | |
320 | 'uploader_url': 'https://triller.co/@statefairent', | |
321 | 'comment_count': int, | |
322 | }, | |
323 | }] | |
324 | ||
325 | def _real_extract(self, url): | |
3d2623a8 | 326 | real_url = self._request_webpage(HEADRequest(url), self._match_id(url)).url |
33b737be | 327 | if self.suitable(real_url): # Prevent infinite loop in case redirect fails |
328 | raise UnsupportedError(real_url) | |
329 | return self.url_result(real_url) |