Skip to content

Commit e6310be

Browse files
davidteatherarhenanarchopythonistaLukasOpp
authored
V7.0.0 - feat: New Playlist Class, fixes bot detection, update docstring (#1215)
* fix: unable to find byted acrawler * fix: playwright timeout sessions with provide opt-out params when creating sessions (#1196) * timeout: 300s -> 30s * fix: tiktok returned invalid reponse by bot detection (#1197) * chore: move wait for load state between mouse moves * Update docstring for create_session() (#1202) * Add playlist class (#1207) * feat: add playlist class * bump version, fix tests --------- Co-authored-by: Rahmat Slamet <[email protected]> Co-authored-by: brandon <[email protected]> Co-authored-by: Lukas <[email protected]>
1 parent a4079f0 commit e6310be

26 files changed

+358
-73
lines changed

Diff for: .sphinx/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
project = "TikTokAPI"
1717
copyright = "2023, David Teather"
1818
author = "David Teather"
19-
release = "v6.5.2"
19+
release = "v7.0.0"
2020

2121
# -- General configuration ---------------------------------------------------
2222
# https://www.sphinx-doc.org/en/main/usage/configuration.html#general-configuration

Diff for: CITATION.cff

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ authors:
55
orcid: "https://orcid.org/0000-0002-9467-4676"
66
title: "TikTokAPI"
77
url: "https://github.com/davidteather/tiktok-api"
8-
version: 6.5.2
9-
date-released: 2024-08-24
8+
version: 7.0.0
9+
date-released: 2025-01-20

Diff for: README.md

+13-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ This is an unofficial api wrapper for TikTok.com in python. With this api you ar
44

55
[![DOI](https://zenodo.org/badge/188710490.svg)](https://zenodo.org/badge/latestdoi/188710490) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/davidteather/) [![Sponsor Me](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/davidteather) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/releases) [![GitHub](https://img.shields.io/github/license/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/blob/main/LICENSE) [![Downloads](https://pepy.tech/badge/tiktokapi)](https://pypi.org/project/TikTokApi/) ![](https://visitor-badge.laobi.icu/badge?page_id=davidteather.TikTok-Api) [![Support Server](https://img.shields.io/discord/783108952111579166.svg?color=7289da&logo=discord&style=flat-square)](https://discord.gg/yyPhbfma6f)
66

7-
This api is designed to **retrieve data** TikTok. It **can not be used post or upload** content to TikTok on the behalf of a user. It has **no support any user-authenticated routes**, if you can't access it while being logged out on their website you can't access it here.
7+
This api is designed to **retrieve data** TikTok. It **can not be used post or upload** content to TikTok on the behalf of a user. It has **no support for any user-authenticated routes**, if you can't access it while being logged out on their website you can't access it here.
88

99
## Sponsors
1010

11-
These sponsors have paid to be placed here and beyond that I do not have any affiliation with them, the TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather).
11+
These sponsors have paid to be placed here or are my own affiliate links which I may earn a commission from, and beyond that I do not have any affiliation with them. The TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather).
1212

1313
<div align="center">
1414
<a href="https://tikapi.io/?ref=davidteather" target="_blank">
@@ -33,6 +33,14 @@ These sponsors have paid to be placed here and beyond that I do not have any aff
3333
<b>TikTok Captcha Solver: </b> Bypass any TikTok captcha in just two lines of code.<br> Scale your TikTok automation and get unblocked with SadCaptcha.
3434
</div>
3535
</a>
36+
<br>
37+
<a href="https://www.webshare.io/?referral_code=3x5812idzzzp" target="_blank">
38+
<img src="https://raw.githubusercontent.com/davidteather/TikTok-Api/main/imgs/webshare.png" width="100" alt="TikTok Captcha Solver">
39+
<b></b>
40+
<div>
41+
<b>Cheap, Reliable Proxies: </b> Supercharge your web scraping with fast, reliable proxies. Try 10 free datacenter proxies today!
42+
</div>
43+
</a>
3644
</div>
3745

3846
## Table of Contents
@@ -93,7 +101,8 @@ docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py
93101

94102
### Common Issues
95103

96-
Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.
104+
- **EmptyResponseException** - this means TikTok is blocking the request and detects you're a bot. This can be a problem with your setup or the library itself
105+
- you may need a proxy to successfuly scrape TikTok, I've made a [web scraping lesson](https://github.com/davidteather/everything-web-scraping/tree/main/002-proxies) explaining the differences of "tiers" of proxies, I've personally had success with [webshare's residential proxies](https://www.webshare.io/?referral_code=3x5812idzzzp) (affiliate link), but you might have success on their free data center IPs or a cheaper competitor.
97106

98107
- **Browser Has no Attribute** - make sure you ran `python3 -m playwright install`, if your error persists try the [playwright-python](https://github.com/microsoft/playwright-python) quickstart guide and diagnose issues from there.
99108

@@ -114,7 +123,7 @@ ms_token = os.environ.get("ms_token", None) # get your own ms_token from your co
114123

115124
async def trending_videos():
116125
async with TikTokApi() as api:
117-
await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3)
126+
await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
118127
async for video in api.trending.videos(count=30):
119128
print(video)
120129
print(video.as_dict)

Diff for: TikTokApi/api/playlist.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
3+
from ..exceptions import InvalidResponseException
4+
5+
if TYPE_CHECKING:
6+
from ..tiktok import TikTokApi
7+
from .video import Video
8+
from .user import User
9+
10+
11+
class Playlist:
12+
"""
13+
A TikTok video playlist.
14+
15+
Example Usage:
16+
.. code-block:: python
17+
18+
playlist = api.playlist(id='7426714779919797038')
19+
"""
20+
21+
parent: ClassVar[TikTokApi]
22+
23+
id: Optional[str]
24+
"""The ID of the playlist."""
25+
name: Optional[str]
26+
"""The name of the playlist."""
27+
video_count: Optional[int]
28+
"""The video count of the playlist."""
29+
creator: Optional[User]
30+
"""The creator of the playlist."""
31+
cover_url: Optional[str]
32+
"""The cover URL of the playlist."""
33+
as_dict: dict
34+
"""The raw data associated with this Playlist."""
35+
36+
def __init__(
37+
self,
38+
id: Optional[str] = None,
39+
data: Optional[dict] = None,
40+
):
41+
"""
42+
You must provide the playlist id or playlist data otherwise this
43+
will not function correctly.
44+
"""
45+
46+
if id is None and data.get("id") is None:
47+
raise TypeError("You must provide id parameter.")
48+
49+
self.id = id
50+
51+
if data is not None:
52+
self.as_dict = data
53+
self.__extract_from_data()
54+
55+
async def info(self, **kwargs) -> dict:
56+
"""
57+
Returns a dictionary of information associated with this Playlist.
58+
59+
Returns:
60+
dict: A dictionary of information associated with this Playlist.
61+
62+
Raises:
63+
InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
64+
65+
Example Usage:
66+
.. code-block:: python
67+
68+
user_data = await api.playlist(id='7426714779919797038').info()
69+
"""
70+
71+
id = getattr(self, "id", None)
72+
if not id:
73+
raise TypeError(
74+
"You must provide the playlist id when creating this class to use this method."
75+
)
76+
77+
url_params = {
78+
"mixId": id,
79+
"msToken": kwargs.get("ms_token"),
80+
}
81+
82+
resp = await self.parent.make_request(
83+
url="https://www.tiktok.com/api/mix/detail/",
84+
params=url_params,
85+
headers=kwargs.get("headers"),
86+
session_index=kwargs.get("session_index"),
87+
)
88+
89+
if resp is None:
90+
raise InvalidResponseException(resp, "TikTok returned an invalid response.")
91+
92+
self.as_dict = resp["mixInfo"]
93+
self.__extract_from_data()
94+
return resp
95+
96+
async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:
97+
"""
98+
Returns an iterator of videos in this User's playlist.
99+
100+
Returns:
101+
Iterator[dict]: An iterator of videos in this User's playlist.
102+
103+
Raises:
104+
InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
105+
106+
Example Usage:
107+
.. code-block:: python
108+
109+
playlist_videos = await api.playlist(id='7426714779919797038').videos()
110+
"""
111+
id = getattr(self, "id", None)
112+
if id is None or id == "":
113+
await self.info(**kwargs)
114+
115+
found = 0
116+
while found < count:
117+
params = {
118+
"mixId": id,
119+
"count": min(count, 30),
120+
"cursor": cursor,
121+
}
122+
123+
resp = await self.parent.make_request(
124+
url="https://www.tiktok.com/api/mix/item_list/",
125+
params=params,
126+
headers=kwargs.get("headers"),
127+
session_index=kwargs.get("session_index"),
128+
)
129+
130+
if resp is None:
131+
raise InvalidResponseException(
132+
resp, "TikTok returned an invalid response."
133+
)
134+
135+
for video in resp.get("itemList", []):
136+
yield self.parent.video(data=video)
137+
found += 1
138+
139+
if not resp.get("hasMore", False):
140+
return
141+
142+
cursor = resp.get("cursor")
143+
144+
def __extract_from_data(self):
145+
data = self.as_dict
146+
keys = data.keys()
147+
148+
if "mixInfo" in keys:
149+
data = data["mixInfo"]
150+
151+
self.id = data.get("id", None) or data.get("mixId", None)
152+
self.name = data.get("name", None) or data.get("mixName", None)
153+
self.video_count = data.get("videoCount", None)
154+
self.creator = self.parent.user(data=data.get("creator", {}))
155+
self.cover_url = data.get("cover", None)
156+
157+
if None in [self.id, self.name, self.video_count, self.creator, self.cover_url]:
158+
User.parent.logger.error(
159+
f"Failed to create Playlist with data: {data}\nwhich has keys {data.keys()}"
160+
)
161+
162+
def __repr__(self):
163+
return self.__str__()
164+
165+
def __str__(self):
166+
id = getattr(self, "id", None)
167+
return f"TikTokApi.playlist(id='{id}'')"

Diff for: TikTokApi/api/user.py

+30-28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
if TYPE_CHECKING:
66
from ..tiktok import TikTokApi
77
from .video import Video
8+
from .playlist import Playlist
89

910

1011
class User:
@@ -87,20 +88,21 @@ async def info(self, **kwargs) -> dict:
8788
self.__extract_from_data()
8889
return resp
8990

90-
async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]:
91+
async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[Playlist]:
9192
"""
92-
Returns a dictionary of information associated with this User's playlist.
93+
Returns a user's playlists.
9394
9495
Returns:
95-
dict: A dictionary of information associated with this User's playlist.
96+
async iterator/generator: Yields TikTokApi.playlist objects.
9697
9798
Raises:
9899
InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
99100
100101
Example Usage:
101102
.. code-block:: python
102103
103-
user_data = await api.user(username='therock').playlist()
104+
async for playlist in await api.user(username='therock').playlists():
105+
# do something
104106
"""
105107

106108
sec_uid = getattr(self, "sec_uid", None)
@@ -109,30 +111,30 @@ async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]:
109111
found = 0
110112

111113
while found < count:
112-
params = {
113-
"secUid": sec_uid,
114-
"count": 20,
115-
"cursor": cursor,
116-
}
117-
118-
resp = await self.parent.make_request(
119-
url="https://www.tiktok.com/api/user/playlist",
120-
params=params,
121-
headers=kwargs.get("headers"),
122-
session_index=kwargs.get("session_index"),
123-
)
124-
125-
if resp is None:
126-
raise InvalidResponseException(resp, "TikTok returned an invalid response.")
127-
128-
for playlist in resp.get("playList", []):
129-
yield playlist
130-
found += 1
131-
132-
if not resp.get("hasMore", False):
133-
return
134-
135-
cursor = resp.get("cursor")
114+
params = {
115+
"secUid": self.sec_uid,
116+
"count": min(count, 20),
117+
"cursor": cursor,
118+
}
119+
120+
resp = await self.parent.make_request(
121+
url="https://www.tiktok.com/api/user/playlist",
122+
params=params,
123+
headers=kwargs.get("headers"),
124+
session_index=kwargs.get("session_index"),
125+
)
126+
127+
if resp is None:
128+
raise InvalidResponseException(resp, "TikTok returned an invalid response.")
129+
130+
for playlist in resp.get("playList", []):
131+
yield self.parent.playlist(data=playlist)
132+
found += 1
133+
134+
if not resp.get("hasMore", False):
135+
return
136+
137+
cursor = resp.get("cursor")
136138

137139

138140
async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:

Diff for: TikTokApi/stealth/js/navigator_userAgent.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
navigator_userAgent = """
22
// replace Headless references in default useragent
3-
const current_ua = navigator.userAgent
3+
const current_ua = navigator.userAgent;
44
Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgent', {
5-
get: () => opts.navigator_user_agent || current_ua.replace('HeadlessChrome/', 'Chrome/')
6-
})
7-
5+
get: () => {
6+
try {
7+
if (typeof opts !== 'undefined' && opts.navigator_user_agent) {
8+
return opts.navigator_user_agent;
9+
}
10+
} catch (error) {
11+
console.warn('Error accessing opts:', error);
12+
}
13+
return current_ua.replace('HeadlessChrome/', 'Chrome/');
14+
}
15+
});
816
"""

0 commit comments

Comments
 (0)