tiktok-live-api 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tiktok_live_api-1.0.0/.gitignore +13 -0
- tiktok_live_api-1.0.0/LICENSE +21 -0
- tiktok_live_api-1.0.0/PKG-INFO +255 -0
- tiktok_live_api-1.0.0/README.md +223 -0
- tiktok_live_api-1.0.0/examples/basic.py +61 -0
- tiktok_live_api-1.0.0/examples/captions.py +48 -0
- tiktok_live_api-1.0.0/examples/chat_bot.py +54 -0
- tiktok_live_api-1.0.0/pyproject.toml +68 -0
- tiktok_live_api-1.0.0/tiktok_live_api/__init__.py +50 -0
- tiktok_live_api-1.0.0/tiktok_live_api/captions.py +203 -0
- tiktok_live_api-1.0.0/tiktok_live_api/client.py +238 -0
- tiktok_live_api-1.0.0/tiktok_live_api/py.typed +1 -0
- tiktok_live_api-1.0.0/tiktok_live_api/types.py +113 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TikTool
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiktok-live-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: TikTok LIVE API Client — Real-time chat, gifts, viewers, battles, and live captions from any TikTok livestream. Production-ready managed API.
|
|
5
|
+
Project-URL: Homepage, https://tik.tools
|
|
6
|
+
Project-URL: Documentation, https://tik.tools/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/tiktool/live-python
|
|
8
|
+
Project-URL: Issues, https://github.com/tiktool/live-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/tiktool/live-python/releases
|
|
10
|
+
Author-email: TikTool <support@tik.tools>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: captions,chat,euler-stream,gifts,live-chat,live-streaming,real-time,speech-to-text,tiktok,tiktok-api,tiktok-bot,tiktok-data,tiktok-live,tiktok-live-api,tiktok-live-connector,tiktok-live-python,tiktok-live-sdk,tiktok-tools,transcription,translation,webcast,websocket
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet
|
|
26
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.8
|
|
30
|
+
Requires-Dist: websockets>=11.0
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# tiktok-live-api
|
|
34
|
+
|
|
35
|
+
**TikTok LIVE API Client for Python** — Connect to any TikTok LIVE stream and receive real-time chat messages, gifts, likes, follows, viewer counts, battles, and more.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/tiktok-live-api/)
|
|
38
|
+
[](https://pypi.org/project/tiktok-live-api/)
|
|
39
|
+
[](https://github.com/tiktool/live-python/blob/main/LICENSE)
|
|
40
|
+
|
|
41
|
+
> **Production-ready alternative to the `TikTokLive` library.** Unlike `TikTokLive` which reverse-engineers TikTok's internal protocol and breaks frequently, `tiktok-live-api` connects to TikTool Live's managed API service — 99.9% uptime, zero maintenance. Also available for Node.js: `npm install @tiktool/live`.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install tiktok-live-api
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from tiktok_live_api import TikTokLive
|
|
53
|
+
|
|
54
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
55
|
+
|
|
56
|
+
@client.on("chat")
|
|
57
|
+
def on_chat(event):
|
|
58
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
59
|
+
|
|
60
|
+
@client.on("gift")
|
|
61
|
+
def on_gift(event):
|
|
62
|
+
print(f"{event['user']['uniqueId']} sent {event['giftName']} ({event['diamondCount']} 💎)")
|
|
63
|
+
|
|
64
|
+
@client.on("like")
|
|
65
|
+
def on_like(event):
|
|
66
|
+
print(f"{event['user']['uniqueId']} liked (total: {event['totalLikes']})")
|
|
67
|
+
|
|
68
|
+
@client.on("follow")
|
|
69
|
+
def on_follow(event):
|
|
70
|
+
print(f"{event['user']['uniqueId']} followed!")
|
|
71
|
+
|
|
72
|
+
@client.on("roomUserSeq")
|
|
73
|
+
def on_viewers(event):
|
|
74
|
+
print(f"{event['viewerCount']} viewers watching")
|
|
75
|
+
|
|
76
|
+
client.run()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That's it. No complex setup, no protobuf, no reverse engineering.
|
|
80
|
+
|
|
81
|
+
## Get a Free API Key
|
|
82
|
+
|
|
83
|
+
1. Go to [tik.tools](https://tik.tools)
|
|
84
|
+
2. Sign up (no credit card required)
|
|
85
|
+
3. Copy your API key
|
|
86
|
+
|
|
87
|
+
The free Sandbox tier gives you 50 requests/day and 1 WebSocket connection.
|
|
88
|
+
|
|
89
|
+
## Environment Variable
|
|
90
|
+
|
|
91
|
+
Instead of passing `api_key` directly, you can set it as an environment variable:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Linux / macOS
|
|
95
|
+
export TIKTOOL_API_KEY=your_api_key_here
|
|
96
|
+
|
|
97
|
+
# Windows (CMD)
|
|
98
|
+
set TIKTOOL_API_KEY=your_api_key_here
|
|
99
|
+
|
|
100
|
+
# Windows (PowerShell)
|
|
101
|
+
$env:TIKTOOL_API_KEY="your_api_key_here"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python my_bot.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from tiktok_live_api import TikTokLive
|
|
110
|
+
|
|
111
|
+
# Automatically reads TIKTOOL_API_KEY from environment
|
|
112
|
+
client = TikTokLive("streamer_username")
|
|
113
|
+
client.on("chat", lambda e: print(e["comment"]))
|
|
114
|
+
client.run()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Events
|
|
118
|
+
|
|
119
|
+
| Event | Description | Key Fields |
|
|
120
|
+
|-------|-------------|------------|
|
|
121
|
+
| `chat` | Chat message | `user`, `comment`, `emotes` |
|
|
122
|
+
| `gift` | Virtual gift | `user`, `giftName`, `diamondCount`, `repeatCount` |
|
|
123
|
+
| `like` | Like event | `user`, `likeCount`, `totalLikes` |
|
|
124
|
+
| `follow` | New follower | `user` |
|
|
125
|
+
| `share` | Stream share | `user` |
|
|
126
|
+
| `member` | Viewer joined | `user` |
|
|
127
|
+
| `subscribe` | New subscriber | `user` |
|
|
128
|
+
| `roomUserSeq` | Viewer count | `viewerCount`, `topViewers` |
|
|
129
|
+
| `battle` | Battle event | `type`, `teams`, `scores` |
|
|
130
|
+
| `envelope` | Treasure chest | `diamonds`, `user` |
|
|
131
|
+
| `streamEnd` | Stream ended | `reason` |
|
|
132
|
+
| `connected` | Connected | `uniqueId` |
|
|
133
|
+
| `disconnected` | Disconnected | `uniqueId` |
|
|
134
|
+
| `error` | Error occurred | `error` |
|
|
135
|
+
| `event` | Catch-all | Full raw event |
|
|
136
|
+
|
|
137
|
+
## Live Captions (Speech-to-Text)
|
|
138
|
+
|
|
139
|
+
Transcribe and translate any TikTok LIVE stream in real-time. **This feature is unique to TikTool Live — no other service offers it.**
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from tiktok_live_api import TikTokCaptions
|
|
143
|
+
|
|
144
|
+
captions = TikTokCaptions(
|
|
145
|
+
"streamer_username",
|
|
146
|
+
api_key="YOUR_API_KEY",
|
|
147
|
+
translate="en", # translate to English
|
|
148
|
+
diarization=True, # identify who is speaking
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@captions.on("caption")
|
|
152
|
+
def on_caption(event):
|
|
153
|
+
speaker = event.get("speaker", "")
|
|
154
|
+
text = event["text"]
|
|
155
|
+
is_final = event.get("isFinal", False)
|
|
156
|
+
print(f"[{speaker}] {text}{' ✓' if is_final else '...'}")
|
|
157
|
+
|
|
158
|
+
@captions.on("translation")
|
|
159
|
+
def on_translation(event):
|
|
160
|
+
print(f" → {event['text']}")
|
|
161
|
+
|
|
162
|
+
captions.run()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Async Usage
|
|
166
|
+
|
|
167
|
+
For integration with async frameworks (FastAPI, aiohttp, etc.):
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import asyncio
|
|
171
|
+
from tiktok_live_api import TikTokLive
|
|
172
|
+
|
|
173
|
+
async def main():
|
|
174
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
175
|
+
|
|
176
|
+
@client.on("chat")
|
|
177
|
+
async def on_chat(event):
|
|
178
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
179
|
+
|
|
180
|
+
await client.connect()
|
|
181
|
+
|
|
182
|
+
asyncio.run(main())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Chat Bot Example
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from tiktok_live_api import TikTokLive
|
|
189
|
+
|
|
190
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
191
|
+
gift_leaderboard = {}
|
|
192
|
+
message_count = 0
|
|
193
|
+
|
|
194
|
+
@client.on("chat")
|
|
195
|
+
def on_chat(event):
|
|
196
|
+
global message_count
|
|
197
|
+
message_count += 1
|
|
198
|
+
msg = event["comment"].lower().strip()
|
|
199
|
+
user = event["user"]["uniqueId"]
|
|
200
|
+
|
|
201
|
+
if msg == "!hello":
|
|
202
|
+
print(f">> BOT: Welcome {user}! 👋")
|
|
203
|
+
elif msg == "!stats":
|
|
204
|
+
print(f">> BOT: {message_count} messages, {len(gift_leaderboard)} gifters")
|
|
205
|
+
elif msg == "!top":
|
|
206
|
+
top = sorted(gift_leaderboard.items(), key=lambda x: -x[1])[:5]
|
|
207
|
+
for i, (name, diamonds) in enumerate(top):
|
|
208
|
+
print(f" {i+1}. {name} — {diamonds} 💎")
|
|
209
|
+
|
|
210
|
+
@client.on("gift")
|
|
211
|
+
def on_gift(event):
|
|
212
|
+
user = event["user"]["uniqueId"]
|
|
213
|
+
diamonds = event.get("diamondCount", 0)
|
|
214
|
+
gift_leaderboard[user] = gift_leaderboard.get(user, 0) + diamonds
|
|
215
|
+
|
|
216
|
+
client.run()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Why tiktok-live-api?
|
|
220
|
+
|
|
221
|
+
| | tiktok-live-api | TikTokLive (Python) | tiktok-live-connector (Node.js) |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| **Stability** | ✓ Managed API, 99.9% uptime | ✗ Breaks on TikTok updates | ✗ Breaks on TikTok updates |
|
|
224
|
+
| **Live Captions** | ✓ AI speech-to-text | ✗ | ✗ |
|
|
225
|
+
| **Maintenance** | ✓ Zero — we handle it | ✗ You fix breakages | ✗ You fix breakages |
|
|
226
|
+
| **CAPTCHA Solving** | ✓ Built-in (Pro+) | ✗ | ✗ |
|
|
227
|
+
| **Feed Discovery** | ✓ See who's live | ✗ | ✗ |
|
|
228
|
+
| **Free Tier** | ✓ 50 requests/day | ✓ Free (unreliable) | ✓ Free (unreliable) |
|
|
229
|
+
|
|
230
|
+
## Pricing
|
|
231
|
+
|
|
232
|
+
| Tier | Requests/Day | WebSocket Connections | Price |
|
|
233
|
+
|------|-------------|----------------------|-------|
|
|
234
|
+
| Sandbox | 50 | 1 (60s) | Free |
|
|
235
|
+
| Basic | 10,000 | 3 (8h) | $7/week |
|
|
236
|
+
| Pro | 75,000 | 50 (8h) | $15/week |
|
|
237
|
+
| Ultra | 300,000 | 500 (8h) | $45/week |
|
|
238
|
+
|
|
239
|
+
## Also Available
|
|
240
|
+
|
|
241
|
+
- **Node.js/TypeScript**: [`npm install @tiktool/live`](https://www.npmjs.com/package/@tiktool/live)
|
|
242
|
+
- **Any language**: Connect via WebSocket: `wss://api.tik.tools?uniqueId=USERNAME&apiKey=KEY`
|
|
243
|
+
- **Unreal Engine**: Native C++/Blueprint plugin
|
|
244
|
+
|
|
245
|
+
## Links
|
|
246
|
+
|
|
247
|
+
- **Website**: [tik.tools](https://tik.tools)
|
|
248
|
+
- **Documentation**: [tik.tools/docs](https://tik.tools/docs)
|
|
249
|
+
- **Python Guide**: [tik.tools/guides/python-tiktok-live](https://tik.tools/guides/python-tiktok-live)
|
|
250
|
+
- **GitHub**: [github.com/tiktool/live-python](https://github.com/tiktool/live-python)
|
|
251
|
+
- **npm (Node.js)**: [@tiktool/live](https://www.npmjs.com/package/@tiktool/live)
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# tiktok-live-api
|
|
2
|
+
|
|
3
|
+
**TikTok LIVE API Client for Python** — Connect to any TikTok LIVE stream and receive real-time chat messages, gifts, likes, follows, viewer counts, battles, and more.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/tiktok-live-api/)
|
|
6
|
+
[](https://pypi.org/project/tiktok-live-api/)
|
|
7
|
+
[](https://github.com/tiktool/live-python/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
> **Production-ready alternative to the `TikTokLive` library.** Unlike `TikTokLive` which reverse-engineers TikTok's internal protocol and breaks frequently, `tiktok-live-api` connects to TikTool Live's managed API service — 99.9% uptime, zero maintenance. Also available for Node.js: `npm install @tiktool/live`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install tiktok-live-api
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from tiktok_live_api import TikTokLive
|
|
21
|
+
|
|
22
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
23
|
+
|
|
24
|
+
@client.on("chat")
|
|
25
|
+
def on_chat(event):
|
|
26
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
27
|
+
|
|
28
|
+
@client.on("gift")
|
|
29
|
+
def on_gift(event):
|
|
30
|
+
print(f"{event['user']['uniqueId']} sent {event['giftName']} ({event['diamondCount']} 💎)")
|
|
31
|
+
|
|
32
|
+
@client.on("like")
|
|
33
|
+
def on_like(event):
|
|
34
|
+
print(f"{event['user']['uniqueId']} liked (total: {event['totalLikes']})")
|
|
35
|
+
|
|
36
|
+
@client.on("follow")
|
|
37
|
+
def on_follow(event):
|
|
38
|
+
print(f"{event['user']['uniqueId']} followed!")
|
|
39
|
+
|
|
40
|
+
@client.on("roomUserSeq")
|
|
41
|
+
def on_viewers(event):
|
|
42
|
+
print(f"{event['viewerCount']} viewers watching")
|
|
43
|
+
|
|
44
|
+
client.run()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That's it. No complex setup, no protobuf, no reverse engineering.
|
|
48
|
+
|
|
49
|
+
## Get a Free API Key
|
|
50
|
+
|
|
51
|
+
1. Go to [tik.tools](https://tik.tools)
|
|
52
|
+
2. Sign up (no credit card required)
|
|
53
|
+
3. Copy your API key
|
|
54
|
+
|
|
55
|
+
The free Sandbox tier gives you 50 requests/day and 1 WebSocket connection.
|
|
56
|
+
|
|
57
|
+
## Environment Variable
|
|
58
|
+
|
|
59
|
+
Instead of passing `api_key` directly, you can set it as an environment variable:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Linux / macOS
|
|
63
|
+
export TIKTOOL_API_KEY=your_api_key_here
|
|
64
|
+
|
|
65
|
+
# Windows (CMD)
|
|
66
|
+
set TIKTOOL_API_KEY=your_api_key_here
|
|
67
|
+
|
|
68
|
+
# Windows (PowerShell)
|
|
69
|
+
$env:TIKTOOL_API_KEY="your_api_key_here"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python my_bot.py
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from tiktok_live_api import TikTokLive
|
|
78
|
+
|
|
79
|
+
# Automatically reads TIKTOOL_API_KEY from environment
|
|
80
|
+
client = TikTokLive("streamer_username")
|
|
81
|
+
client.on("chat", lambda e: print(e["comment"]))
|
|
82
|
+
client.run()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Events
|
|
86
|
+
|
|
87
|
+
| Event | Description | Key Fields |
|
|
88
|
+
|-------|-------------|------------|
|
|
89
|
+
| `chat` | Chat message | `user`, `comment`, `emotes` |
|
|
90
|
+
| `gift` | Virtual gift | `user`, `giftName`, `diamondCount`, `repeatCount` |
|
|
91
|
+
| `like` | Like event | `user`, `likeCount`, `totalLikes` |
|
|
92
|
+
| `follow` | New follower | `user` |
|
|
93
|
+
| `share` | Stream share | `user` |
|
|
94
|
+
| `member` | Viewer joined | `user` |
|
|
95
|
+
| `subscribe` | New subscriber | `user` |
|
|
96
|
+
| `roomUserSeq` | Viewer count | `viewerCount`, `topViewers` |
|
|
97
|
+
| `battle` | Battle event | `type`, `teams`, `scores` |
|
|
98
|
+
| `envelope` | Treasure chest | `diamonds`, `user` |
|
|
99
|
+
| `streamEnd` | Stream ended | `reason` |
|
|
100
|
+
| `connected` | Connected | `uniqueId` |
|
|
101
|
+
| `disconnected` | Disconnected | `uniqueId` |
|
|
102
|
+
| `error` | Error occurred | `error` |
|
|
103
|
+
| `event` | Catch-all | Full raw event |
|
|
104
|
+
|
|
105
|
+
## Live Captions (Speech-to-Text)
|
|
106
|
+
|
|
107
|
+
Transcribe and translate any TikTok LIVE stream in real-time. **This feature is unique to TikTool Live — no other service offers it.**
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from tiktok_live_api import TikTokCaptions
|
|
111
|
+
|
|
112
|
+
captions = TikTokCaptions(
|
|
113
|
+
"streamer_username",
|
|
114
|
+
api_key="YOUR_API_KEY",
|
|
115
|
+
translate="en", # translate to English
|
|
116
|
+
diarization=True, # identify who is speaking
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@captions.on("caption")
|
|
120
|
+
def on_caption(event):
|
|
121
|
+
speaker = event.get("speaker", "")
|
|
122
|
+
text = event["text"]
|
|
123
|
+
is_final = event.get("isFinal", False)
|
|
124
|
+
print(f"[{speaker}] {text}{' ✓' if is_final else '...'}")
|
|
125
|
+
|
|
126
|
+
@captions.on("translation")
|
|
127
|
+
def on_translation(event):
|
|
128
|
+
print(f" → {event['text']}")
|
|
129
|
+
|
|
130
|
+
captions.run()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Async Usage
|
|
134
|
+
|
|
135
|
+
For integration with async frameworks (FastAPI, aiohttp, etc.):
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
import asyncio
|
|
139
|
+
from tiktok_live_api import TikTokLive
|
|
140
|
+
|
|
141
|
+
async def main():
|
|
142
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
143
|
+
|
|
144
|
+
@client.on("chat")
|
|
145
|
+
async def on_chat(event):
|
|
146
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
147
|
+
|
|
148
|
+
await client.connect()
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Chat Bot Example
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from tiktok_live_api import TikTokLive
|
|
157
|
+
|
|
158
|
+
client = TikTokLive("streamer_username", api_key="YOUR_API_KEY")
|
|
159
|
+
gift_leaderboard = {}
|
|
160
|
+
message_count = 0
|
|
161
|
+
|
|
162
|
+
@client.on("chat")
|
|
163
|
+
def on_chat(event):
|
|
164
|
+
global message_count
|
|
165
|
+
message_count += 1
|
|
166
|
+
msg = event["comment"].lower().strip()
|
|
167
|
+
user = event["user"]["uniqueId"]
|
|
168
|
+
|
|
169
|
+
if msg == "!hello":
|
|
170
|
+
print(f">> BOT: Welcome {user}! 👋")
|
|
171
|
+
elif msg == "!stats":
|
|
172
|
+
print(f">> BOT: {message_count} messages, {len(gift_leaderboard)} gifters")
|
|
173
|
+
elif msg == "!top":
|
|
174
|
+
top = sorted(gift_leaderboard.items(), key=lambda x: -x[1])[:5]
|
|
175
|
+
for i, (name, diamonds) in enumerate(top):
|
|
176
|
+
print(f" {i+1}. {name} — {diamonds} 💎")
|
|
177
|
+
|
|
178
|
+
@client.on("gift")
|
|
179
|
+
def on_gift(event):
|
|
180
|
+
user = event["user"]["uniqueId"]
|
|
181
|
+
diamonds = event.get("diamondCount", 0)
|
|
182
|
+
gift_leaderboard[user] = gift_leaderboard.get(user, 0) + diamonds
|
|
183
|
+
|
|
184
|
+
client.run()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Why tiktok-live-api?
|
|
188
|
+
|
|
189
|
+
| | tiktok-live-api | TikTokLive (Python) | tiktok-live-connector (Node.js) |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| **Stability** | ✓ Managed API, 99.9% uptime | ✗ Breaks on TikTok updates | ✗ Breaks on TikTok updates |
|
|
192
|
+
| **Live Captions** | ✓ AI speech-to-text | ✗ | ✗ |
|
|
193
|
+
| **Maintenance** | ✓ Zero — we handle it | ✗ You fix breakages | ✗ You fix breakages |
|
|
194
|
+
| **CAPTCHA Solving** | ✓ Built-in (Pro+) | ✗ | ✗ |
|
|
195
|
+
| **Feed Discovery** | ✓ See who's live | ✗ | ✗ |
|
|
196
|
+
| **Free Tier** | ✓ 50 requests/day | ✓ Free (unreliable) | ✓ Free (unreliable) |
|
|
197
|
+
|
|
198
|
+
## Pricing
|
|
199
|
+
|
|
200
|
+
| Tier | Requests/Day | WebSocket Connections | Price |
|
|
201
|
+
|------|-------------|----------------------|-------|
|
|
202
|
+
| Sandbox | 50 | 1 (60s) | Free |
|
|
203
|
+
| Basic | 10,000 | 3 (8h) | $7/week |
|
|
204
|
+
| Pro | 75,000 | 50 (8h) | $15/week |
|
|
205
|
+
| Ultra | 300,000 | 500 (8h) | $45/week |
|
|
206
|
+
|
|
207
|
+
## Also Available
|
|
208
|
+
|
|
209
|
+
- **Node.js/TypeScript**: [`npm install @tiktool/live`](https://www.npmjs.com/package/@tiktool/live)
|
|
210
|
+
- **Any language**: Connect via WebSocket: `wss://api.tik.tools?uniqueId=USERNAME&apiKey=KEY`
|
|
211
|
+
- **Unreal Engine**: Native C++/Blueprint plugin
|
|
212
|
+
|
|
213
|
+
## Links
|
|
214
|
+
|
|
215
|
+
- **Website**: [tik.tools](https://tik.tools)
|
|
216
|
+
- **Documentation**: [tik.tools/docs](https://tik.tools/docs)
|
|
217
|
+
- **Python Guide**: [tik.tools/guides/python-tiktok-live](https://tik.tools/guides/python-tiktok-live)
|
|
218
|
+
- **GitHub**: [github.com/tiktool/live-python](https://github.com/tiktool/live-python)
|
|
219
|
+
- **npm (Node.js)**: [@tiktool/live](https://www.npmjs.com/package/@tiktool/live)
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Basic example — connect to a TikTok LIVE stream and print all events.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
# Set environment variables first:
|
|
5
|
+
# Linux/macOS: export TIKTOOL_API_KEY=your_key TIKTOK_USERNAME=streamer
|
|
6
|
+
# Windows CMD: set TIKTOOL_API_KEY=your_key & set TIKTOK_USERNAME=streamer
|
|
7
|
+
# PowerShell: $env:TIKTOOL_API_KEY="your_key"; $env:TIKTOK_USERNAME="streamer"
|
|
8
|
+
|
|
9
|
+
python basic.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from tiktok_live_api import TikTokLive
|
|
16
|
+
|
|
17
|
+
USERNAME = os.environ.get("TIKTOK_USERNAME", "")
|
|
18
|
+
if not USERNAME:
|
|
19
|
+
print("Set TIKTOK_USERNAME environment variable to the streamer's username.")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
client = TikTokLive(USERNAME)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@client.on("connected")
|
|
26
|
+
def on_connected(event):
|
|
27
|
+
print(f"Connected to @{event['uniqueId']}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@client.on("chat")
|
|
31
|
+
def on_chat(event):
|
|
32
|
+
print(f"[chat] {event['user']['uniqueId']}: {event['comment']}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@client.on("gift")
|
|
36
|
+
def on_gift(event):
|
|
37
|
+
diamonds = event.get("diamondCount", 0)
|
|
38
|
+
print(f"[gift] {event['user']['uniqueId']} sent {event['giftName']} ({diamonds} diamonds)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@client.on("like")
|
|
42
|
+
def on_like(event):
|
|
43
|
+
print(f"[like] {event['user']['uniqueId']} liked (total: {event['totalLikes']})")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@client.on("follow")
|
|
47
|
+
def on_follow(event):
|
|
48
|
+
print(f"[follow] {event['user']['uniqueId']} followed")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@client.on("roomUserSeq")
|
|
52
|
+
def on_viewers(event):
|
|
53
|
+
print(f"[viewers] {event['viewerCount']} watching")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@client.on("streamEnd")
|
|
57
|
+
def on_stream_end(event):
|
|
58
|
+
print("[stream] Stream has ended.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
client.run()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Live Captions example — transcribe and translate a TikTok LIVE stream.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
# Set environment variables first:
|
|
5
|
+
# Linux/macOS: export TIKTOOL_API_KEY=your_key TIKTOK_USERNAME=streamer
|
|
6
|
+
# Windows CMD: set TIKTOOL_API_KEY=your_key & set TIKTOK_USERNAME=streamer
|
|
7
|
+
# PowerShell: $env:TIKTOOL_API_KEY="your_key"; $env:TIKTOK_USERNAME="streamer"
|
|
8
|
+
|
|
9
|
+
python captions.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from tiktok_live_api import TikTokCaptions
|
|
16
|
+
|
|
17
|
+
USERNAME = os.environ.get("TIKTOK_USERNAME", "")
|
|
18
|
+
if not USERNAME:
|
|
19
|
+
print("Set TIKTOK_USERNAME environment variable to the streamer's username.")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
captions = TikTokCaptions(
|
|
23
|
+
USERNAME,
|
|
24
|
+
translate="en",
|
|
25
|
+
diarization=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@captions.on("connected")
|
|
30
|
+
def on_connected(event):
|
|
31
|
+
print(f"Listening to @{event['uniqueId']}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@captions.on("caption")
|
|
35
|
+
def on_caption(event):
|
|
36
|
+
speaker = event.get("speaker", "")
|
|
37
|
+
text = event["text"]
|
|
38
|
+
is_final = event.get("isFinal", False)
|
|
39
|
+
status = "FINAL" if is_final else "partial"
|
|
40
|
+
print(f"[{status}] [{speaker}] {text}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@captions.on("translation")
|
|
44
|
+
def on_translation(event):
|
|
45
|
+
print(f" -> {event['text']}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
captions.run()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Chat bot example — respond to commands and track a gift leaderboard.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
# Set environment variables first:
|
|
5
|
+
# Linux/macOS: export TIKTOOL_API_KEY=your_key TIKTOK_USERNAME=streamer
|
|
6
|
+
# Windows CMD: set TIKTOOL_API_KEY=your_key & set TIKTOK_USERNAME=streamer
|
|
7
|
+
# PowerShell: $env:TIKTOOL_API_KEY="your_key"; $env:TIKTOK_USERNAME="streamer"
|
|
8
|
+
|
|
9
|
+
python chat_bot.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Dict
|
|
15
|
+
|
|
16
|
+
from tiktok_live_api import TikTokLive
|
|
17
|
+
|
|
18
|
+
USERNAME = os.environ.get("TIKTOK_USERNAME", "")
|
|
19
|
+
if not USERNAME:
|
|
20
|
+
print("Set TIKTOK_USERNAME environment variable to the streamer's username.")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
client = TikTokLive(USERNAME)
|
|
24
|
+
|
|
25
|
+
gift_leaderboard: Dict[str, int] = {}
|
|
26
|
+
message_count = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@client.on("chat")
|
|
30
|
+
def on_chat(event):
|
|
31
|
+
global message_count
|
|
32
|
+
message_count += 1
|
|
33
|
+
msg = event["comment"].lower().strip()
|
|
34
|
+
user = event["user"]["uniqueId"]
|
|
35
|
+
|
|
36
|
+
if msg == "!hello":
|
|
37
|
+
print(f">> BOT: Welcome {user}!")
|
|
38
|
+
elif msg == "!stats":
|
|
39
|
+
print(f">> BOT: {message_count} messages, {len(gift_leaderboard)} gifters")
|
|
40
|
+
elif msg == "!top":
|
|
41
|
+
top = sorted(gift_leaderboard.items(), key=lambda x: -x[1])[:5]
|
|
42
|
+
for rank, (name, diamonds) in enumerate(top, start=1):
|
|
43
|
+
print(f" {rank}. {name} - {diamonds} diamonds")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@client.on("gift")
|
|
47
|
+
def on_gift(event):
|
|
48
|
+
user = event["user"]["uniqueId"]
|
|
49
|
+
diamonds = event.get("diamondCount", 0)
|
|
50
|
+
gift_leaderboard[user] = gift_leaderboard.get(user, 0) + diamonds
|
|
51
|
+
print(f"[gift] {user} sent {event['giftName']} ({diamonds} diamonds)")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
client.run()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tiktok-live-api"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "TikTok LIVE API Client — Real-time chat, gifts, viewers, battles, and live captions from any TikTok livestream. Production-ready managed API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "TikTool", email = "support@tik.tools"}
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"tiktok",
|
|
17
|
+
"tiktok-live",
|
|
18
|
+
"tiktok-api",
|
|
19
|
+
"tiktok-live-api",
|
|
20
|
+
"tiktok-live-connector",
|
|
21
|
+
"live-streaming",
|
|
22
|
+
"websocket",
|
|
23
|
+
"chat",
|
|
24
|
+
"gifts",
|
|
25
|
+
"webcast",
|
|
26
|
+
"real-time",
|
|
27
|
+
"speech-to-text",
|
|
28
|
+
"captions",
|
|
29
|
+
"transcription",
|
|
30
|
+
"translation",
|
|
31
|
+
"tiktok-data",
|
|
32
|
+
"tiktok-bot",
|
|
33
|
+
"live-chat",
|
|
34
|
+
"tiktok-live-python",
|
|
35
|
+
"tiktok-tools",
|
|
36
|
+
"euler-stream",
|
|
37
|
+
"tiktok-live-sdk"
|
|
38
|
+
]
|
|
39
|
+
classifiers = [
|
|
40
|
+
"Development Status :: 5 - Production/Stable",
|
|
41
|
+
"Intended Audience :: Developers",
|
|
42
|
+
"License :: OSI Approved :: MIT License",
|
|
43
|
+
"Operating System :: OS Independent",
|
|
44
|
+
"Programming Language :: Python :: 3",
|
|
45
|
+
"Programming Language :: Python :: 3.8",
|
|
46
|
+
"Programming Language :: Python :: 3.9",
|
|
47
|
+
"Programming Language :: Python :: 3.10",
|
|
48
|
+
"Programming Language :: Python :: 3.11",
|
|
49
|
+
"Programming Language :: Python :: 3.12",
|
|
50
|
+
"Programming Language :: Python :: 3.13",
|
|
51
|
+
"Topic :: Internet",
|
|
52
|
+
"Topic :: Software Development :: Libraries",
|
|
53
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
54
|
+
"Typing :: Typed",
|
|
55
|
+
]
|
|
56
|
+
dependencies = [
|
|
57
|
+
"websockets>=11.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.urls]
|
|
61
|
+
Homepage = "https://tik.tools"
|
|
62
|
+
Documentation = "https://tik.tools/docs"
|
|
63
|
+
Repository = "https://github.com/tiktool/live-python"
|
|
64
|
+
Issues = "https://github.com/tiktool/live-python/issues"
|
|
65
|
+
Changelog = "https://github.com/tiktool/live-python/releases"
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.wheel]
|
|
68
|
+
packages = ["tiktok_live_api"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""TikTok Live API — TikTok LIVE stream data client for Python.
|
|
2
|
+
|
|
3
|
+
Connect to any TikTok LIVE stream and receive real-time events:
|
|
4
|
+
chat messages, gifts, likes, follows, viewer counts, battles, and more.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from tiktok_live_api import TikTokLive
|
|
9
|
+
|
|
10
|
+
client = TikTokLive("streamer_username", api_key="YOUR_KEY")
|
|
11
|
+
|
|
12
|
+
@client.on("chat")
|
|
13
|
+
def on_chat(event):
|
|
14
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
15
|
+
|
|
16
|
+
client.run()
|
|
17
|
+
|
|
18
|
+
See https://tik.tools/docs for full documentation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from tiktok_live_api.client import TikTokLive
|
|
22
|
+
from tiktok_live_api.captions import TikTokCaptions
|
|
23
|
+
from tiktok_live_api.types import (
|
|
24
|
+
TikTokUser,
|
|
25
|
+
ChatEvent,
|
|
26
|
+
GiftEvent,
|
|
27
|
+
LikeEvent,
|
|
28
|
+
MemberEvent,
|
|
29
|
+
SocialEvent,
|
|
30
|
+
RoomUserSeqEvent,
|
|
31
|
+
BattleEvent,
|
|
32
|
+
CaptionEvent,
|
|
33
|
+
TranslationEvent,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"TikTokLive",
|
|
38
|
+
"TikTokCaptions",
|
|
39
|
+
"TikTokUser",
|
|
40
|
+
"ChatEvent",
|
|
41
|
+
"GiftEvent",
|
|
42
|
+
"LikeEvent",
|
|
43
|
+
"MemberEvent",
|
|
44
|
+
"SocialEvent",
|
|
45
|
+
"RoomUserSeqEvent",
|
|
46
|
+
"BattleEvent",
|
|
47
|
+
"CaptionEvent",
|
|
48
|
+
"TranslationEvent",
|
|
49
|
+
]
|
|
50
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""TikTokCaptions — Real-time AI speech-to-text for TikTok LIVE streams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
import websockets
|
|
13
|
+
import websockets.client
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("tiktok_live_api.captions")
|
|
16
|
+
|
|
17
|
+
EventHandler = Callable[[Dict[str, Any]], None]
|
|
18
|
+
AsyncEventHandler = Callable[[Dict[str, Any]], Any]
|
|
19
|
+
AnyHandler = Union[EventHandler, AsyncEventHandler]
|
|
20
|
+
|
|
21
|
+
CAPTIONS_BASE = "wss://api.tik.tools/captions"
|
|
22
|
+
_VERSION = "1.0.0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TikTokCaptions:
|
|
26
|
+
"""Real-time AI speech-to-text transcription for TikTok LIVE streams.
|
|
27
|
+
|
|
28
|
+
This is a unique feature not available in any other TikTok LIVE library.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
unique_id: TikTok username (without @).
|
|
32
|
+
api_key: Your TikTool API key. Get one at https://tik.tools
|
|
33
|
+
translate: Target language code for real-time translation (e.g. ``"en"``, ``"es"``).
|
|
34
|
+
diarization: Enable speaker identification (default ``True``).
|
|
35
|
+
|
|
36
|
+
Example::
|
|
37
|
+
|
|
38
|
+
from tiktok_live_api import TikTokCaptions
|
|
39
|
+
|
|
40
|
+
captions = TikTokCaptions("streamer", api_key="KEY", translate="en")
|
|
41
|
+
|
|
42
|
+
@captions.on("caption")
|
|
43
|
+
def on_caption(event):
|
|
44
|
+
print(f"[{event.get('speaker', '')}] {event['text']}")
|
|
45
|
+
|
|
46
|
+
captions.run()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
unique_id: str,
|
|
52
|
+
*,
|
|
53
|
+
api_key: Optional[str] = None,
|
|
54
|
+
translate: Optional[str] = None,
|
|
55
|
+
diarization: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.unique_id = unique_id.lstrip("@")
|
|
58
|
+
self.api_key = api_key or os.environ.get("TIKTOOL_API_KEY", "")
|
|
59
|
+
if not self.api_key:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"api_key is required. Get a free key at https://tik.tools"
|
|
62
|
+
)
|
|
63
|
+
self.translate = translate
|
|
64
|
+
self.diarization = diarization
|
|
65
|
+
|
|
66
|
+
self._handlers: Dict[str, List[AnyHandler]] = {}
|
|
67
|
+
self._ws: Optional[websockets.client.WebSocketClientProtocol] = None
|
|
68
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
69
|
+
self._connected = False
|
|
70
|
+
self._intentional_close = False
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def connected(self) -> bool:
|
|
74
|
+
"""Whether the captions client is currently connected."""
|
|
75
|
+
return self._connected
|
|
76
|
+
|
|
77
|
+
def on(
|
|
78
|
+
self, event: str, handler: Optional[AnyHandler] = None
|
|
79
|
+
) -> Any:
|
|
80
|
+
"""Register an event handler. Can be used as a decorator or called directly.
|
|
81
|
+
|
|
82
|
+
Supported events:
|
|
83
|
+
caption, translation, credits, status, error, connected, disconnected
|
|
84
|
+
|
|
85
|
+
Usage as decorator::
|
|
86
|
+
|
|
87
|
+
@captions.on("caption")
|
|
88
|
+
def on_caption(event):
|
|
89
|
+
print(event["text"])
|
|
90
|
+
|
|
91
|
+
Usage as method::
|
|
92
|
+
|
|
93
|
+
captions.on("translation", lambda e: print(e["text"]))
|
|
94
|
+
"""
|
|
95
|
+
if handler is not None:
|
|
96
|
+
self._handlers.setdefault(event, []).append(handler)
|
|
97
|
+
return handler
|
|
98
|
+
|
|
99
|
+
def decorator(fn: AnyHandler) -> AnyHandler:
|
|
100
|
+
self._handlers.setdefault(event, []).append(fn)
|
|
101
|
+
return fn
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
104
|
+
|
|
105
|
+
def _emit(self, event: str, data: Any = None) -> None:
|
|
106
|
+
for handler in self._handlers.get(event, []):
|
|
107
|
+
try:
|
|
108
|
+
result = handler(data)
|
|
109
|
+
if asyncio.iscoroutine(result):
|
|
110
|
+
loop = self._loop or asyncio.get_event_loop()
|
|
111
|
+
loop.create_task(result)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.error("Error in '%s' handler: %s", event, exc)
|
|
114
|
+
|
|
115
|
+
async def start(self) -> None:
|
|
116
|
+
"""Start receiving captions from the stream.
|
|
117
|
+
|
|
118
|
+
For most use cases, prefer :meth:`run` which handles the event loop
|
|
119
|
+
automatically. Use ``start`` when integrating with an existing
|
|
120
|
+
async application.
|
|
121
|
+
"""
|
|
122
|
+
self._intentional_close = False
|
|
123
|
+
self._loop = asyncio.get_event_loop()
|
|
124
|
+
params = f"uniqueId={self.unique_id}&apiKey={self.api_key}"
|
|
125
|
+
if self.translate:
|
|
126
|
+
params += f"&translate={self.translate}"
|
|
127
|
+
if self.diarization:
|
|
128
|
+
params += "&diarization=true"
|
|
129
|
+
|
|
130
|
+
uri = f"{CAPTIONS_BASE}?{params}"
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
self._ws = await websockets.connect(
|
|
134
|
+
uri,
|
|
135
|
+
additional_headers={"User-Agent": f"tiktok-live-api-python/{_VERSION}"},
|
|
136
|
+
ping_interval=10,
|
|
137
|
+
ping_timeout=30,
|
|
138
|
+
)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
self._emit("error", {"error": str(exc)})
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
self._connected = True
|
|
144
|
+
self._emit("connected", {"uniqueId": self.unique_id})
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
async for raw in self._ws:
|
|
148
|
+
try:
|
|
149
|
+
event = json.loads(raw)
|
|
150
|
+
except (json.JSONDecodeError, TypeError):
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
msg_type = event.get("type", "unknown")
|
|
154
|
+
self._emit(msg_type, event)
|
|
155
|
+
except websockets.ConnectionClosed:
|
|
156
|
+
pass
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
self._emit("error", {"error": str(exc)})
|
|
159
|
+
finally:
|
|
160
|
+
self._connected = False
|
|
161
|
+
self._emit("disconnected", {"uniqueId": self.unique_id})
|
|
162
|
+
|
|
163
|
+
def stop(self) -> None:
|
|
164
|
+
"""Stop receiving captions."""
|
|
165
|
+
self._intentional_close = True
|
|
166
|
+
if self._ws is not None:
|
|
167
|
+
if self._loop is not None and self._loop.is_running():
|
|
168
|
+
self._loop.create_task(self._ws.close())
|
|
169
|
+
else:
|
|
170
|
+
try:
|
|
171
|
+
asyncio.get_event_loop().run_until_complete(self._ws.close())
|
|
172
|
+
except RuntimeError:
|
|
173
|
+
pass
|
|
174
|
+
self._connected = False
|
|
175
|
+
|
|
176
|
+
def run(self) -> None:
|
|
177
|
+
"""Start receiving captions and block until stopped.
|
|
178
|
+
|
|
179
|
+
Creates an event loop, connects to the captions stream, and blocks
|
|
180
|
+
until the stream ends or :meth:`stop` is called::
|
|
181
|
+
|
|
182
|
+
from tiktok_live_api import TikTokCaptions
|
|
183
|
+
|
|
184
|
+
captions = TikTokCaptions("streamer", api_key="KEY", translate="en")
|
|
185
|
+
captions.on("caption", lambda e: print(e["text"]))
|
|
186
|
+
captions.run()
|
|
187
|
+
"""
|
|
188
|
+
loop = asyncio.new_event_loop()
|
|
189
|
+
asyncio.set_event_loop(loop)
|
|
190
|
+
self._loop = loop
|
|
191
|
+
|
|
192
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
193
|
+
try:
|
|
194
|
+
loop.add_signal_handler(sig, self.stop)
|
|
195
|
+
except (NotImplementedError, ValueError, OSError):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
loop.run_until_complete(self.start())
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
self.stop()
|
|
202
|
+
finally:
|
|
203
|
+
loop.close()
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""TikTokLive client — connect to any TikTok LIVE stream via WebSocket."""
|
|
2
|
+
# ruff: noqa: E402
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import signal
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
import websockets
|
|
14
|
+
import websockets.client
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("tiktok_live_api")
|
|
17
|
+
|
|
18
|
+
EventHandler = Callable[[Dict[str, Any]], None]
|
|
19
|
+
AsyncEventHandler = Callable[[Dict[str, Any]], Any]
|
|
20
|
+
AnyHandler = Union[EventHandler, AsyncEventHandler]
|
|
21
|
+
|
|
22
|
+
WS_BASE = "wss://api.tik.tools"
|
|
23
|
+
_VERSION = "1.0.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TikTokLive:
|
|
27
|
+
"""Connect to a TikTok LIVE stream and receive real-time events.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
unique_id: TikTok username (without @).
|
|
31
|
+
api_key: Your TikTool API key. Get one free at https://tik.tools
|
|
32
|
+
auto_reconnect: Auto-reconnect on disconnect (default True).
|
|
33
|
+
max_reconnect_attempts: Max reconnection attempts (default 5).
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
from tiktok_live_api import TikTokLive
|
|
38
|
+
|
|
39
|
+
client = TikTokLive("streamer_username", api_key="YOUR_KEY")
|
|
40
|
+
|
|
41
|
+
@client.on("chat")
|
|
42
|
+
def on_chat(event):
|
|
43
|
+
print(f"{event['user']['uniqueId']}: {event['comment']}")
|
|
44
|
+
|
|
45
|
+
client.run()
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
unique_id: str,
|
|
51
|
+
*,
|
|
52
|
+
api_key: Optional[str] = None,
|
|
53
|
+
auto_reconnect: bool = True,
|
|
54
|
+
max_reconnect_attempts: int = 5,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.unique_id = unique_id.lstrip("@")
|
|
57
|
+
self.api_key = api_key or os.environ.get("TIKTOOL_API_KEY", "")
|
|
58
|
+
if not self.api_key:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"api_key is required. Get a free key at https://tik.tools"
|
|
61
|
+
)
|
|
62
|
+
self.auto_reconnect = auto_reconnect
|
|
63
|
+
self.max_reconnect_attempts = max_reconnect_attempts
|
|
64
|
+
|
|
65
|
+
self._handlers: Dict[str, List[AnyHandler]] = {}
|
|
66
|
+
self._ws: Optional[websockets.client.WebSocketClientProtocol] = None
|
|
67
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
68
|
+
self._connected = False
|
|
69
|
+
self._intentional_close = False
|
|
70
|
+
self._reconnect_attempts = 0
|
|
71
|
+
self._event_count = 0
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def connected(self) -> bool:
|
|
75
|
+
"""Whether the client is currently connected."""
|
|
76
|
+
return self._connected
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def event_count(self) -> int:
|
|
80
|
+
"""Total number of events received."""
|
|
81
|
+
return self._event_count
|
|
82
|
+
|
|
83
|
+
def on(
|
|
84
|
+
self, event: str, handler: Optional[AnyHandler] = None
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""Register an event handler. Can be used as a decorator or called directly.
|
|
87
|
+
|
|
88
|
+
Supported events:
|
|
89
|
+
connected, chat, gift, like, follow, share, member, subscribe,
|
|
90
|
+
roomUserSeq, roomInfo, battle, envelope, streamEnd, error,
|
|
91
|
+
disconnected, event (catch-all)
|
|
92
|
+
|
|
93
|
+
Usage as decorator::
|
|
94
|
+
|
|
95
|
+
@client.on("chat")
|
|
96
|
+
def on_chat(event):
|
|
97
|
+
print(event["comment"])
|
|
98
|
+
|
|
99
|
+
Usage as method::
|
|
100
|
+
|
|
101
|
+
client.on("gift", lambda e: print(e["giftName"]))
|
|
102
|
+
"""
|
|
103
|
+
if handler is not None:
|
|
104
|
+
self._handlers.setdefault(event, []).append(handler)
|
|
105
|
+
return handler
|
|
106
|
+
|
|
107
|
+
def decorator(fn: AnyHandler) -> AnyHandler:
|
|
108
|
+
self._handlers.setdefault(event, []).append(fn)
|
|
109
|
+
return fn
|
|
110
|
+
|
|
111
|
+
return decorator
|
|
112
|
+
|
|
113
|
+
def _emit(self, event: str, data: Any = None) -> None:
|
|
114
|
+
for handler in self._handlers.get(event, []):
|
|
115
|
+
try:
|
|
116
|
+
result = handler(data)
|
|
117
|
+
if asyncio.iscoroutine(result):
|
|
118
|
+
loop = self._loop or asyncio.get_event_loop()
|
|
119
|
+
loop.create_task(result)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
logger.error("Error in '%s' handler: %s", event, exc)
|
|
122
|
+
|
|
123
|
+
async def connect(self) -> None:
|
|
124
|
+
"""Connect to the TikTok LIVE stream.
|
|
125
|
+
|
|
126
|
+
For most use cases, prefer :meth:`run` which handles the event loop
|
|
127
|
+
setup automatically. Use ``connect`` directly when integrating with
|
|
128
|
+
an existing async application (FastAPI, aiohttp, etc.)::
|
|
129
|
+
|
|
130
|
+
import asyncio
|
|
131
|
+
from tiktok_live_api import TikTokLive
|
|
132
|
+
|
|
133
|
+
async def main():
|
|
134
|
+
client = TikTokLive("username", api_key="KEY")
|
|
135
|
+
client.on("chat", lambda e: print(e["comment"]))
|
|
136
|
+
await client.connect()
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
"""
|
|
140
|
+
self._intentional_close = False
|
|
141
|
+
self._loop = asyncio.get_event_loop()
|
|
142
|
+
uri = f"{WS_BASE}?uniqueId={self.unique_id}&apiKey={self.api_key}"
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
self._ws = await websockets.connect(
|
|
146
|
+
uri,
|
|
147
|
+
additional_headers={"User-Agent": f"tiktok-live-api-python/{_VERSION}"},
|
|
148
|
+
ping_interval=10,
|
|
149
|
+
ping_timeout=30,
|
|
150
|
+
close_timeout=5,
|
|
151
|
+
)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
self._emit("error", {"error": str(exc)})
|
|
154
|
+
raise
|
|
155
|
+
|
|
156
|
+
self._connected = True
|
|
157
|
+
self._reconnect_attempts = 0
|
|
158
|
+
self._emit("connected", {"uniqueId": self.unique_id})
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
async for raw in self._ws:
|
|
162
|
+
try:
|
|
163
|
+
event = json.loads(raw)
|
|
164
|
+
except (json.JSONDecodeError, TypeError):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
self._event_count += 1
|
|
168
|
+
event_type = event.get("event", "unknown")
|
|
169
|
+
data = event.get("data", event)
|
|
170
|
+
|
|
171
|
+
self._emit("event", event)
|
|
172
|
+
self._emit(event_type, data)
|
|
173
|
+
except websockets.ConnectionClosed:
|
|
174
|
+
pass
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
self._emit("error", {"error": str(exc)})
|
|
177
|
+
finally:
|
|
178
|
+
self._connected = False
|
|
179
|
+
self._emit("disconnected", {"uniqueId": self.unique_id})
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
not self._intentional_close
|
|
183
|
+
and self.auto_reconnect
|
|
184
|
+
and self._reconnect_attempts < self.max_reconnect_attempts
|
|
185
|
+
):
|
|
186
|
+
self._reconnect_attempts += 1
|
|
187
|
+
delay = min(2 ** (self._reconnect_attempts - 1), 30)
|
|
188
|
+
logger.info(
|
|
189
|
+
"Reconnecting in %ds (attempt %d/%d)...",
|
|
190
|
+
delay,
|
|
191
|
+
self._reconnect_attempts,
|
|
192
|
+
self.max_reconnect_attempts,
|
|
193
|
+
)
|
|
194
|
+
await asyncio.sleep(delay)
|
|
195
|
+
await self.connect()
|
|
196
|
+
|
|
197
|
+
def disconnect(self) -> None:
|
|
198
|
+
"""Disconnect from the stream."""
|
|
199
|
+
self._intentional_close = True
|
|
200
|
+
if self._ws is not None:
|
|
201
|
+
if self._loop is not None and self._loop.is_running():
|
|
202
|
+
self._loop.create_task(self._ws.close())
|
|
203
|
+
else:
|
|
204
|
+
try:
|
|
205
|
+
asyncio.get_event_loop().run_until_complete(self._ws.close())
|
|
206
|
+
except RuntimeError:
|
|
207
|
+
pass
|
|
208
|
+
self._connected = False
|
|
209
|
+
|
|
210
|
+
def run(self) -> None:
|
|
211
|
+
"""Connect and block until disconnected.
|
|
212
|
+
|
|
213
|
+
This is the simplest way to use the client. It creates an event loop,
|
|
214
|
+
connects to the stream, and blocks until the stream ends or
|
|
215
|
+
:meth:`disconnect` is called::
|
|
216
|
+
|
|
217
|
+
from tiktok_live_api import TikTokLive
|
|
218
|
+
|
|
219
|
+
client = TikTokLive("streamer", api_key="KEY")
|
|
220
|
+
client.on("chat", lambda e: print(e["comment"]))
|
|
221
|
+
client.run()
|
|
222
|
+
"""
|
|
223
|
+
loop = asyncio.new_event_loop()
|
|
224
|
+
asyncio.set_event_loop(loop)
|
|
225
|
+
self._loop = loop
|
|
226
|
+
|
|
227
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
228
|
+
try:
|
|
229
|
+
loop.add_signal_handler(sig, self.disconnect)
|
|
230
|
+
except (NotImplementedError, ValueError, OSError):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
loop.run_until_complete(self.connect())
|
|
235
|
+
except KeyboardInterrupt:
|
|
236
|
+
self.disconnect()
|
|
237
|
+
finally:
|
|
238
|
+
loop.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
py_typed
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Type definitions for TikTok LIVE API events.
|
|
2
|
+
|
|
3
|
+
These types can be used for IDE autocompletion and type checking
|
|
4
|
+
when handling events from :class:`~tiktok_live_api.TikTokLive` and
|
|
5
|
+
:class:`~tiktok_live_api.TikTokCaptions`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
if sys.version_info >= (3, 8):
|
|
14
|
+
from typing import TypedDict
|
|
15
|
+
else:
|
|
16
|
+
from typing_extensions import TypedDict
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"TikTokUser",
|
|
20
|
+
"ChatEvent",
|
|
21
|
+
"GiftEvent",
|
|
22
|
+
"LikeEvent",
|
|
23
|
+
"MemberEvent",
|
|
24
|
+
"SocialEvent",
|
|
25
|
+
"RoomUserSeqEvent",
|
|
26
|
+
"BattleEvent",
|
|
27
|
+
"CaptionEvent",
|
|
28
|
+
"TranslationEvent",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TikTokUser(TypedDict, total=False):
|
|
33
|
+
"""User profile attached to most LIVE events."""
|
|
34
|
+
|
|
35
|
+
userId: str
|
|
36
|
+
uniqueId: str
|
|
37
|
+
nickname: str
|
|
38
|
+
profilePictureUrl: str
|
|
39
|
+
followRole: int
|
|
40
|
+
isSubscriber: bool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ChatEvent(TypedDict, total=False):
|
|
44
|
+
"""Payload for ``chat`` events."""
|
|
45
|
+
|
|
46
|
+
user: TikTokUser
|
|
47
|
+
comment: str
|
|
48
|
+
emotes: List[Dict[str, Any]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GiftEvent(TypedDict, total=False):
|
|
52
|
+
"""Payload for ``gift`` events."""
|
|
53
|
+
|
|
54
|
+
user: TikTokUser
|
|
55
|
+
giftId: int
|
|
56
|
+
giftName: str
|
|
57
|
+
diamondCount: int
|
|
58
|
+
repeatCount: int
|
|
59
|
+
repeatEnd: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LikeEvent(TypedDict, total=False):
|
|
63
|
+
"""Payload for ``like`` events."""
|
|
64
|
+
|
|
65
|
+
user: TikTokUser
|
|
66
|
+
likeCount: int
|
|
67
|
+
totalLikes: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MemberEvent(TypedDict, total=False):
|
|
71
|
+
"""Payload for ``member`` (viewer join) events."""
|
|
72
|
+
|
|
73
|
+
user: TikTokUser
|
|
74
|
+
actionId: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SocialEvent(TypedDict, total=False):
|
|
78
|
+
"""Payload for ``follow`` and ``share`` events."""
|
|
79
|
+
|
|
80
|
+
user: TikTokUser
|
|
81
|
+
eventType: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RoomUserSeqEvent(TypedDict, total=False):
|
|
85
|
+
"""Payload for ``roomUserSeq`` (viewer count) events."""
|
|
86
|
+
|
|
87
|
+
viewerCount: int
|
|
88
|
+
topViewers: List[TikTokUser]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BattleEvent(TypedDict, total=False):
|
|
92
|
+
"""Payload for ``battle`` events."""
|
|
93
|
+
|
|
94
|
+
type: str
|
|
95
|
+
teams: List[Dict[str, Any]]
|
|
96
|
+
scores: List[int]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CaptionEvent(TypedDict, total=False):
|
|
100
|
+
"""Payload for ``caption`` events from :class:`~tiktok_live_api.TikTokCaptions`."""
|
|
101
|
+
|
|
102
|
+
text: str
|
|
103
|
+
speaker: str
|
|
104
|
+
isFinal: bool
|
|
105
|
+
language: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TranslationEvent(TypedDict, total=False):
|
|
109
|
+
"""Payload for ``translation`` events from :class:`~tiktok_live_api.TikTokCaptions`."""
|
|
110
|
+
|
|
111
|
+
text: str
|
|
112
|
+
sourceLanguage: str
|
|
113
|
+
targetLanguage: str
|