[ie/tiktok] Add playlist support

pull/14152/head
Ilya Gulya 1 month ago
parent 3bd9154412
commit 341be5ff25
No known key found for this signature in database

@ -2100,6 +2100,7 @@ from .tiktok import (
TikTokEffectIE, TikTokEffectIE,
TikTokIE, TikTokIE,
TikTokLiveIE, TikTokLiveIE,
TikTokPlaylistIE,
TikTokSoundIE, TikTokSoundIE,
TikTokTagIE, TikTokTagIE,
TikTokUserIE, TikTokUserIE,

@ -1458,6 +1458,156 @@ class TikTokVMIE(InfoExtractor):
return self.url_result(new_url) return self.url_result(new_url)
class TikTokPlaylistIE(TikTokBaseIE):
IE_NAME = 'tiktok:playlist'
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/@(?P<user_id>[\w.-]+)/playlist/(?P<playlist_name>[^/?#]+)-(?P<id>\d+)/?(?:[?#]|$)'
_TESTS = [{
'url': 'https://www.tiktok.com/@berzerg86/playlist/Cycles-of-Humanity-7515108740543056683',
'info_dict': {
'id': '7515108740543056683',
'title': 'berzerg86-Cycles of Humanity',
'description': 'TikTok playlist with 14 videos',
},
'playlist_mincount': 1,
}, {
'url': 'https://www.tiktok.com/@linustech/playlist/Riley%20Tech%20Tips-7220878800733260549',
'info_dict': {
'id': '7220878800733260549',
'title': 'linustech-Riley Tech Tips',
'description': 'TikTok playlist with 2 videos',
},
'playlist_mincount': 1,
}]
def _call_playlist_api(self, endpoint, mix_id, query=None):
"""Call TikTok's playlist API endpoints"""
base_query = {
'aid': '1988',
'app_language': 'en',
'app_name': 'tiktok_web',
'browser_language': 'en-US',
'browser_name': 'Mozilla',
'browser_online': 'true',
'browser_platform': 'Win32',
'browser_version': '5.0 (Windows)',
'channel': 'tiktok_web',
'cookie_enabled': 'true',
'device_id': self._DEVICE_ID,
'device_platform': 'web_pc',
'focus_state': 'true',
'history_len': '2',
'is_fullscreen': 'false',
'is_page_visible': 'true',
'language': 'en',
'mixId': mix_id,
'os': 'windows',
'priority_region': '',
'referer': '',
'region': 'US',
'screen_height': '1080',
'screen_width': '1920',
'tz_name': 'UTC',
'webcast_language': 'en',
}
if query:
base_query.update(query)
return self._download_json(
f'https://www.tiktok.com/api/{endpoint}/', mix_id,
f'Downloading {endpoint} data',
query=base_query, fatal=False)
def _real_extract(self, url):
user_id, playlist_name, playlist_id = self._match_valid_url(url).group('user_id', 'playlist_name', 'id')
# First get playlist metadata
playlist_meta = self._call_playlist_api('mix/detail', playlist_id)
if not playlist_meta:
raise ExtractorError('Unable to fetch playlist metadata')
# Check if the request was successful
if traverse_obj(playlist_meta, 'statusCode') != 0:
status_msg = traverse_obj(playlist_meta, 'status_msg', default='Unknown error')
raise ExtractorError(f'Playlist API error: {status_msg}')
# Extract playlist info
mix_info = traverse_obj(playlist_meta, 'mixInfo', expected_type=dict)
if not mix_info:
raise ExtractorError('No playlist info found')
playlist_title = traverse_obj(mix_info, 'name', expected_type=str) or playlist_name
# Now get the video list
entries = []
cursor = 0
count = 30 # Max items per request
while True:
item_list_response = self._call_playlist_api('mix/item_list', playlist_id, {
'cursor': cursor,
'count': count,
})
if not item_list_response or traverse_obj(item_list_response, 'statusCode') != 0:
break
item_list = traverse_obj(item_list_response, 'itemList', expected_type=list)
if not item_list:
break
for video in item_list:
video_id = traverse_obj(video, 'id', expected_type=str)
if not video_id:
continue
author_info = traverse_obj(video, ('author', {
'uploader': ('uniqueId', {str}),
'uploader_id': ('id', {str}),
}), get_all=False) or {}
video_uploader = author_info.get('uploader') or user_id
video_url = f'https://www.tiktok.com/@{video_uploader}/video/{video_id}'
# Create a basic info dict for flat extraction
video_info = {
'id': video_id,
'title': traverse_obj(video, 'desc', expected_type=str) or f'TikTok video #{video_id}',
'uploader': video_uploader,
'uploader_id': author_info.get('uploader_id'),
'timestamp': traverse_obj(video, 'createTime', expected_type=int),
'view_count': traverse_obj(video, ('stats', 'playCount'), expected_type=int),
'like_count': traverse_obj(video, ('stats', 'diggCount'), expected_type=int),
'comment_count': traverse_obj(video, ('stats', 'commentCount'), expected_type=int),
'repost_count': traverse_obj(video, ('stats', 'shareCount'), expected_type=int),
'duration': traverse_obj(video, ('video', 'duration'), expected_type=int),
'thumbnail': traverse_obj(video, ('video', 'cover'), expected_type=str),
}
entries.append(self.url_result(video_url, TikTokIE, **video_info))
# Check if there are more videos to fetch
has_more = traverse_obj(item_list_response, 'hasMore', expected_type=bool)
if not has_more:
break
# Update cursor for next page
cursor = traverse_obj(item_list_response, 'cursor', expected_type=str)
if not cursor:
break
try:
cursor = int(cursor)
except (ValueError, TypeError):
break
if not entries:
raise ExtractorError('No videos found in playlist')
return self.playlist_result(
entries, playlist_id, f'{user_id}-{playlist_title}',
description=f'TikTok playlist with {len(entries)} videos')
class TikTokLiveIE(TikTokBaseIE): class TikTokLiveIE(TikTokBaseIE):
_VALID_URL = r'''(?x)https?://(?: _VALID_URL = r'''(?x)https?://(?:
(?:www\.)?tiktok\.com/@(?P<uploader>[\w.-]+)/live| (?:www\.)?tiktok\.com/@(?P<uploader>[\w.-]+)/live|

Loading…
Cancel
Save