]>
jfr.im git - yt-dlp.git/blob - yt_dlp/extractor/tiktok.py
953ff05b6ea0c7bd402d4fb396d7257a37706759
2 from __future__
import unicode_literals
9 from .common
import InfoExtractor
20 class TikTokIE(InfoExtractor
):
21 _VALID_URL
= r
'https?://www\.tiktok\.com/@[\w\.-]+/video/(?P<id>\d+)'
24 'url': 'https://www.tiktok.com/@leenabhushan/video/6748451240264420610',
25 'md5': '34a7543afd5a151b0840ba6736fb633b',
27 'id': '6748451240264420610',
29 'title': '#jassmanak #lehanga #leenabhushan',
30 'description': '#jassmanak #lehanga #leenabhushan',
34 'uploader': 'leenabhushan',
35 'uploader_id': '6691488002098119685',
36 'uploader_url': 'https://www.tiktok.com/@leenabhushan',
37 'creator': 'facestoriesbyleenabh',
38 'thumbnail': r
're:^https?://[\w\/\.\-]+(~[\w\-]+\.image)?',
39 'upload_date': '20191016',
40 'timestamp': 1571246252,
47 'url': 'https://www.tiktok.com/@patroxofficial/video/6742501081818877190?langCountry=en',
48 'md5': '06b9800d47d5fe51a19e322dd86e61c9',
50 'id': '6742501081818877190',
52 'title': 'md5:5e2a23877420bb85ce6521dbee39ba94',
53 'description': 'md5:5e2a23877420bb85ce6521dbee39ba94',
58 'uploader_id': '18702747',
59 'uploader_url': 'https://www.tiktok.com/@patrox',
61 'thumbnail': r
're:^https?://[\w\/\.\-]+(~[\w\-]+\.image)?',
62 'upload_date': '20190930',
63 'timestamp': 1569860870,
71 'url': 'https://www.tiktok.com/@MS4wLjABAAAAAR29F6J2Ktu0Daw03BJyXPNoRQ-W7U5a0Mn3lVCq2rQhjOd_WNLclHUoFgwX8Eno/video/6932675057474981122',
72 'only_matching': True,
74 _APP_VERSION
= '20.9.3'
75 _MANIFEST_APP_VERSION
= '291'
76 QUALITIES
= ('360p', '540p', '720p')
78 def _extract_aweme(self
, props_data
, webpage
, url
):
80 props_data
, lambda x
: x
['pageProps']['itemInfo']['itemStruct'], dict)
81 author_info
= try_get(
82 props_data
, lambda x
: x
['pageProps']['itemInfo']['itemStruct']['author'], dict) or {}
84 props_data
, lambda x
: x
['pageProps']['itemInfo']['itemStruct']['music'], dict) or {}
85 stats_info
= try_get(props_data
, lambda x
: x
['pageProps']['itemInfo']['itemStruct']['stats'], dict) or {}
87 user_id
= str_or_none(author_info
.get('uniqueId'))
88 download_url
= try_get(video_info
, (lambda x
: x
['video']['playAddr'],
89 lambda x
: x
['video']['downloadAddr']))
90 height
= try_get(video_info
, lambda x
: x
['video']['height'], int)
91 width
= try_get(video_info
, lambda x
: x
['video']['width'], int)
93 'url': video_info
.get('thumbnail') or self
._og
_search
_thumbnail
(webpage
),
97 tracker
= try_get(props_data
, lambda x
: x
['initialProps']['$wid'])
100 'id': str_or_none(video_info
.get('id')),
105 'title': video_info
.get('desc') or self
._og
_search
_title
(webpage
),
106 'duration': try_get(video_info
, lambda x
: x
['video']['duration'], int),
107 'view_count': int_or_none(stats_info
.get('playCount')),
108 'like_count': int_or_none(stats_info
.get('diggCount')),
109 'repost_count': int_or_none(stats_info
.get('shareCount')),
110 'comment_count': int_or_none(stats_info
.get('commentCount')),
111 'timestamp': try_get(video_info
, lambda x
: int(x
['createTime']), int),
112 'creator': str_or_none(author_info
.get('nickname')),
114 'uploader_id': str_or_none(author_info
.get('id')),
115 'uploader_url': f
'https://www.tiktok.com/@{user_id}',
116 'track': str_or_none(music_info
.get('title')),
117 'album': str_or_none(music_info
.get('album')) or None,
118 'artist': str_or_none(music_info
.get('authorName')),
119 'thumbnails': thumbnails
,
120 'description': str_or_none(video_info
.get('desc')),
121 'webpage_url': self
._og
_search
_url
(webpage
),
124 'Cookie': 'tt_webid=%s; tt_webid_v2=%s' % (tracker
, tracker
),
128 def _extract_aweme_app(self
, aweme_id
):
130 'aweme_id': aweme_id
,
131 'version_name': self
._APP
_VERSION
,
132 'version_code': self
._MANIFEST
_APP
_VERSION
,
133 'build_number': self
._APP
_VERSION
,
134 'manifest_version_code': self
._MANIFEST
_APP
_VERSION
,
135 'update_version_code': self
._MANIFEST
_APP
_VERSION
,
136 'openudid': ''.join(random
.choice('0123456789abcdef') for i
in range(16)),
137 'uuid': ''.join([random
.choice(string
.digits
) for num
in range(16)]),
138 '_rticket': int(time
.time() * 1000),
139 'ts': int(time
.time()),
140 'device_brand': 'Google',
141 'device_type': 'Pixel 4',
142 'device_platform': 'android',
143 'resolution': '1080*1920',
147 'carrier_region': 'US',
151 'app_language': 'en',
153 'timezone_name': 'America/New_York',
154 'timezone_offset': '-14400',
155 'channel': 'googleplay',
162 'cp': 'cbfhckdckkde1',
165 self
._set
_cookie
('.tiktokv.com', 'odin_tt', ''.join(random
.choice('0123456789abcdef') for i
in range(160)))
167 aweme_detail
= self
._download
_json
(
168 'https://api-t2.tiktokv.com/aweme/v1/aweme/detail/', aweme_id
,
169 'Downloading video details', 'Unable to download video details',
171 'User-Agent': f
'com.ss.android.ugc.trill/{self._MANIFEST_APP_VERSION} (Linux; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)',
172 }, query
=query
)['aweme_detail']
173 video_info
= aweme_detail
['video']
175 def parse_url_key(url_key
):
176 format_id
, codec
, res
, bitrate
= self
._search
_regex
(
177 r
'v[^_]+_(?P<id>(?P<codec>[^_]+)_(?P<res>\d+p)_(?P<bitrate>\d+))', url_key
,
178 'url key', default
=(None, None, None, None), group
=('id', 'codec', 'res', 'bitrate'))
182 'format_id': format_id
,
183 'vcodec': 'h265' if codec
== 'bytevc1' else codec
,
184 'tbr': int_or_none(bitrate
, scale
=1000) or None,
185 'quality': qualities(self
.QUALITIES
)(res
),
188 known_resolutions
= {}
190 def extract_addr(addr
, add_meta
={}):
191 parsed_meta
, res
= parse_url_key(addr
.get('url_key', ''))
193 known_resolutions
.setdefault(res
, {}).setdefault('height', add_meta
.get('height'))
194 known_resolutions
[res
].setdefault('width', add_meta
.get('width'))
195 parsed_meta
.update(known_resolutions
.get(res
, {}))
196 add_meta
.setdefault('height', int_or_none(res
[:-1]))
199 'filesize': int_or_none(addr
.get('data_size')),
202 **add_meta
, **parsed_meta
203 } for url
in addr
.get('url_list') or []]
205 # Hack: Add direct video links first to prioritize them when removing duplicate formats
207 if video_info
.get('play_addr'):
208 formats
.extend(extract_addr(video_info
['play_addr'], {
209 'format_id': 'play_addr',
210 'format_note': 'Direct video',
211 'vcodec': 'h265' if traverse_obj(
212 video_info
, 'is_bytevc1', 'is_h265') else 'h264', # Always h264?
213 'width': video_info
.get('width'),
214 'height': video_info
.get('height'),
216 if video_info
.get('download_addr'):
217 formats
.extend(extract_addr(video_info
['download_addr'], {
218 'format_id': 'download_addr',
219 'format_note': 'Download video%s' % (', watermarked' if video_info
.get('has_watermark') else ''),
221 'width': video_info
.get('width'),
222 'height': video_info
.get('height'),
223 'source_preference': -2 if video_info
.get('has_watermark') else -1,
225 if video_info
.get('play_addr_h264'):
226 formats
.extend(extract_addr(video_info
['play_addr_h264'], {
227 'format_id': 'play_addr_h264',
228 'format_note': 'Direct video',
231 if video_info
.get('play_addr_bytevc1'):
232 formats
.extend(extract_addr(video_info
['play_addr_bytevc1'], {
233 'format_id': 'play_addr_bytevc1',
234 'format_note': 'Direct video',
238 for bitrate
in video_info
.get('bit_rate', []):
239 if bitrate
.get('play_addr'):
240 formats
.extend(extract_addr(bitrate
['play_addr'], {
241 'format_id': bitrate
.get('gear_name'),
242 'format_note': 'Playback video',
243 'tbr': try_get(bitrate
, lambda x
: x
['bit_rate'] / 1000),
244 'vcodec': 'h265' if traverse_obj(
245 bitrate
, 'is_bytevc1', 'is_h265') else 'h264',
248 self
._remove
_duplicate
_formats
(formats
)
249 self
._sort
_formats
(formats
, ('quality', 'source', 'codec', 'size', 'br'))
252 for cover_id
in ('cover', 'ai_dynamic_cover', 'animated_cover', 'ai_dynamic_cover_bak',
253 'origin_cover', 'dynamic_cover'):
254 cover
= video_info
.get(cover_id
)
256 for cover_url
in cover
['url_list']:
262 stats_info
= aweme_detail
.get('statistics', {})
263 author_info
= aweme_detail
.get('author', {})
264 music_info
= aweme_detail
.get('music', {})
265 user_id
= str_or_none(author_info
.get('nickname'))
267 contained_music_track
= traverse_obj(
268 music_info
, ('matched_song', 'title'), ('matched_pgc_sound', 'title'), expected_type
=str)
269 contained_music_author
= traverse_obj(
270 music_info
, ('matched_song', 'author'), ('matched_pgc_sound', 'author'), 'author', expected_type
=str)
272 is_generic_og_trackname
= music_info
.get('is_original_sound') and music_info
.get('title') == 'original sound - %s' % music_info
.get('owner_handle')
273 if is_generic_og_trackname
:
274 music_track
, music_author
= contained_music_track
or 'original sound', contained_music_author
276 music_track
, music_author
= music_info
.get('title'), music_info
.get('author')
280 'title': aweme_detail
['desc'],
281 'description': aweme_detail
['desc'],
282 'view_count': int_or_none(stats_info
.get('play_count')),
283 'like_count': int_or_none(stats_info
.get('digg_count')),
284 'repost_count': int_or_none(stats_info
.get('share_count')),
285 'comment_count': int_or_none(stats_info
.get('comment_count')),
286 'uploader': str_or_none(author_info
.get('unique_id')),
288 'uploader_id': str_or_none(author_info
.get('uid')),
289 'uploader_url': f
'https://www.tiktok.com/@{user_id}' if user_id
else None,
290 'track': music_track
,
291 'album': str_or_none(music_info
.get('album')) or None,
292 'artist': music_author
,
293 'timestamp': int_or_none(aweme_detail
.get('create_time')),
295 'thumbnails': thumbnails
,
296 'duration': int_or_none(traverse_obj(video_info
, 'duration', ('download_addr', 'duration')), scale
=1000)
299 def _real_extract(self
, url
):
300 video_id
= self
._match
_id
(url
)
303 return self
._extract
_aweme
_app
(video_id
)
304 except ExtractorError
as e
:
305 self
.report_warning(f
'{e}; Retrying with webpage')
307 # If we only call once, we get a 403 when downlaoding the video.
308 self
._download
_webpage
(url
, video_id
)
309 webpage
= self
._download
_webpage
(url
, video_id
, note
='Downloading video webpage')
310 json_string
= self
._search
_regex
(
311 r
'id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P<json_string_ld>[^<]+)',
312 webpage
, 'json_string', group
='json_string_ld')
313 json_data
= self
._parse
_json
(json_string
, video_id
)
314 props_data
= try_get(json_data
, lambda x
: x
['props'], expected_type
=dict)
316 # Chech statusCode for success
317 status
= props_data
.get('pageProps').get('statusCode')
319 return self
._extract
_aweme
(props_data
, webpage
, url
)
320 elif status
== 10216:
321 raise ExtractorError('This video is private', expected
=True)
323 raise ExtractorError('Video not available', video_id
=video_id
)
326 class TikTokUserIE(InfoExtractor
):
327 IE_NAME
= 'tiktok:user'
328 _VALID_URL
= r
'https?://(?:www\.)?tiktok\.com/@(?P<id>[\w\._]+)/?(?:$|[#?])'
330 'url': 'https://tiktok.com/@corgibobaa?lang=en',
331 'playlist_mincount': 45,
333 'id': '6935371178089399301',
335 'skip': 'Cookies (not necessarily logged in) are needed.'
337 'url': 'https://www.tiktok.com/@meme',
338 'playlist_mincount': 593,
340 'id': '79005827461758976',
342 'skip': 'Cookies (not necessarily logged in) are needed.'
345 def _entries(self
, url
, user_id
):
346 webpage
= self
._download
_webpage
(url
, user_id
)
347 own_id
= self
._search
_regex
(r
'\"id\":\"(?P<userid>\d+)', webpage
, user_id
, default
=None)
349 raise ExtractorError('Cookies (not necessarily logged in) are needed.', expected
=True)
350 secuid
= self
._search
_regex
(r
'\"secUid\":\"(?P<secUid>[^\"]+)', webpage
, user_id
)
351 verifyfp_cookie
= self
._get
_cookies
('https://www.tiktok.com').get('s_v_web_id')
352 if not verifyfp_cookie
:
353 raise ExtractorError('Improper cookies (missing s_v_web_id).', expected
=True)
354 api_url
= f
'https://m.tiktok.com/api/post/item_list/?aid=1988&cookie_enabled=true&count=30&verifyFp={verifyfp_cookie.value}&secUid={secuid}&cursor='
356 for page
in itertools
.count():
357 data_json
= self
._download
_json
(api_url
+ cursor
, user_id
, note
='Downloading Page %d' % page
)
358 for video
in data_json
.get('itemList', []):
359 video_id
= video
['id']
360 video_url
= f
'https://www.tiktok.com/@{user_id}/video/{video_id}'
361 yield self
._url
_result
(video_url
, 'TikTok', video_id
, str_or_none(video
.get('desc')))
362 if not data_json
.get('hasMore'):
364 cursor
= data_json
['cursor']
366 def _real_extract(self
, url
):
367 user_id
= self
._match
_id
(url
)
368 return self
.playlist_result(self
._entries
(url
, user_id
), user_id
)