tiktok-live-api 1.2.18 โ 1.3.1
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.
- package/LICENSE +21 -21
- package/README.md +39 -75
- package/dist/index.d.mts +61 -1
- package/dist/index.d.ts +61 -1
- package/package.json +1 -5
- package/bin/demo.mjs +0 -685
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 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.
|
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="banner.png" alt="tiktok-live-api" width="100%" />
|
|
2
|
+
<img src="https://raw.githubusercontent.com/tiktool/tiktok-live-api/main/banner.png" alt="tiktok-live-api" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# tiktok-live-api
|
|
6
6
|
|
|
7
|
+
> ๐ **Full documentation, guides, and dashboard โ [tik.tools](https://tik.tools)** | ๐ **Python SDK โ [tik.tools/guides/python-tiktok-live](https://tik.tools/guides/python-tiktok-live)** | ๐ **WebSocket API โ [tik.tools/websocket](https://tik.tools/websocket)**
|
|
8
|
+
|
|
7
9
|
**Unofficial TikTok LIVE API Client for Node.js & TypeScript** โ Connect to any TikTok LIVE stream and receive real-time chat messages, gifts, likes, follows, viewer counts, battles, and more. Includes AI-powered live captions (speech-to-text). Powered by the [TikTool](https://tik.tools) managed API.
|
|
8
10
|
|
|
9
11
|
[](https://www.npmjs.com/package/tiktok-live-api)
|
|
@@ -18,17 +20,6 @@
|
|
|
18
20
|
|
|
19
21
|
> This package is **not affiliated with or endorsed by TikTok**. It connects to the [TikTool Live](https://tik.tools) managed API service โ 99.9% uptime, no reverse engineering, no maintenance required. Also available for [Python](https://pypi.org/project/tiktok-live-api/) and [any language via WebSocket](https://tik.tools/docs).
|
|
20
22
|
|
|
21
|
-
## ๐ One-Command Quick Start
|
|
22
|
-
|
|
23
|
-
Instantly connect to a live TikTok stream and print real-time events to your terminal. No signup, no install:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx tiktok-live-api
|
|
27
|
-
```
|
|
28
|
-
*Or connect to a specific stream:* `npx tiktok-live-api @username`
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
23
|
## Install
|
|
33
24
|
|
|
34
25
|
```bash
|
|
@@ -200,7 +191,9 @@ client.connect();
|
|
|
200
191
|
| `member` | Viewer joined | `user` |
|
|
201
192
|
| `subscribe` | New subscriber | `user` |
|
|
202
193
|
| `roomUserSeq` | Viewer count | `viewerCount`, `topViewers` |
|
|
203
|
-
| `battle` |
|
|
194
|
+
| `battle` | PK start / end / status change | `battleId`, `status` (1=ACTIVE / 2=STARTING / 3=ENDED / 4=PREPARING), `battleDuration`, `teams` |
|
|
195
|
+
| `battleArmies` | Live PK score update | `battleId`, `status`, `matchId`, `sessionId`, `durationSec`, `secsRemaining`, `hosts[]` โ each host has `teamTotalScore` + `contributors[]` (MVP first) |
|
|
196
|
+
| `battleItemCard` | Booster multipliers, gloves, mist, match-guide, thunder, extra-time | `effect` (`'gloves'` / `'mist'` / `'booster_x2'` / `'booster_x3'` / `'match_guide'` / ...), `multiplier` (2 or 3), `senderUserId`, `senderNickname`, `activatedAtSec`, `durationSec`, `endsAtSec`, `commentTemplate` |
|
|
204
197
|
| `roomPin` | Pinned/starred message | `user`, `comment`, `action`, `durationSeconds` |
|
|
205
198
|
| `envelope` | Treasure chest | `diamonds`, `user` |
|
|
206
199
|
| `streamEnd` | Stream ended | `reason` |
|
|
@@ -211,80 +204,44 @@ client.connect();
|
|
|
211
204
|
|
|
212
205
|
All events are fully typed with TypeScript interfaces. Your IDE will show autocompletion for every field.
|
|
213
206
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Transcribe and translate any TikTok LIVE stream in real-time. **This feature is unique to TikTool Live โ no other TikTok library offers it.**
|
|
207
|
+
### Battle / PK example
|
|
217
208
|
|
|
218
209
|
```typescript
|
|
219
|
-
import {
|
|
210
|
+
import { TikTokLive } from 'tiktok-live-api';
|
|
220
211
|
|
|
221
|
-
|
|
222
|
-
live.on('event', (event) => {
|
|
223
|
-
console.log(event.type, event);
|
|
224
|
-
});
|
|
225
|
-
```
|
|
212
|
+
const client = new TikTokLive({ uniqueId: 'creator_username', apiKey: 'tk_...' });
|
|
226
213
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
| Event | Type | Description | Fields |
|
|
230
|
-
|-------|------|-------------|--------|
|
|
231
|
-
| `chat` | `ChatEvent` | Chat message | `user`, `comment`, `starred?` |
|
|
232
|
-
| `member` | `MemberEvent` | User joined | `user`, `action` |
|
|
233
|
-
| `like` | `LikeEvent` | User liked | `user`, `likeCount`, `totalLikes` |
|
|
234
|
-
| `gift` | `GiftEvent` | Gift sent | `user`, `giftName`, `diamondCount`, `repeatCount`, `combo` |
|
|
235
|
-
| `social` | `SocialEvent` | Follow / Share | `user`, `action` |
|
|
236
|
-
| `roomUserSeq` | `RoomUserSeqEvent` | Viewer count | `viewerCount`, `totalViewers` |
|
|
237
|
-
| `battle` | `BattleEvent` | Link Mic battle | `status` |
|
|
238
|
-
| `battleArmies` | `BattleArmiesEvent` | Battle teams | โ |
|
|
239
|
-
| `subscribe` | `SubscribeEvent` | New subscriber | `user`, `subMonth` |
|
|
240
|
-
| `emoteChat` | `EmoteChatEvent` | Emote in chat | `user`, `emoteId` |
|
|
241
|
-
| `envelope` | `EnvelopeEvent` | Treasure chest | `diamondCount` |
|
|
242
|
-
| `question` | `QuestionEvent` | Q&A question | `user`, `questionText` |
|
|
243
|
-
| `control` | `ControlEvent` | Stream control | `action` (3 = ended) |
|
|
244
|
-
| `room` | `RoomEvent` | Room status | `status` |
|
|
245
|
-
| `liveIntro` | `LiveIntroEvent` | Stream intro | `title` |
|
|
246
|
-
| `rankUpdate` | `RankUpdateEvent` | Rank update | `rankType` |
|
|
247
|
-
| `linkMic` | `LinkMicEvent` | Link Mic | `action` |
|
|
248
|
-
| `roomPin` | `RoomPinEvent` | Pinned/starred message | `user`, `comment`, `action`, `durationSeconds` |
|
|
249
|
-
| `unknown` | `UnknownEvent` | Unrecognized | `method` |
|
|
250
|
-
|
|
251
|
-
### Connection Events
|
|
252
|
-
|
|
253
|
-
| Event | Callback | Description |
|
|
254
|
-
|-------|----------|-------------|
|
|
255
|
-
| `connected` | `() => void` | Connected to stream |
|
|
256
|
-
| `disconnected` | `(code, reason) => void` | Disconnected |
|
|
257
|
-
| `roomInfo` | `(info: RoomInfo) => void` | Room info |
|
|
258
|
-
| `error` | `(error: Error) => void` | Error |
|
|
214
|
+
client.on('battle', e => console.log('PK', e.status, e.battleId, 'duration=', e.battleDuration));
|
|
259
215
|
|
|
260
|
-
|
|
216
|
+
client.on('battleArmies', e => {
|
|
217
|
+
console.log('Countdown:', e.secsRemaining, 's');
|
|
218
|
+
for (const host of e.hosts ?? []) {
|
|
219
|
+
console.log(`@${host.hostUserId} team total=${host.teamTotalScore}`);
|
|
220
|
+
const mvp = host.contributors[0];
|
|
221
|
+
if (mvp) console.log(` MVP @${mvp.nickname} score=${mvp.score}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
261
224
|
|
|
262
|
-
|
|
225
|
+
client.on('battleItemCard', e => {
|
|
226
|
+
if (e.multiplier > 0) console.log(`x${e.multiplier} booster from @${e.senderUniqueId}`);
|
|
227
|
+
else console.log(`Effect ${e.effect} from @${e.senderUniqueId} (${e.durationSec}s)`);
|
|
228
|
+
});
|
|
263
229
|
|
|
264
|
-
|
|
230
|
+
await client.connect();
|
|
231
|
+
```
|
|
265
232
|
|
|
266
|
-
- **Auto-detect language** โ Automatically identifies the spoken language
|
|
267
|
-
- **Speaker diarization** โ Identifies individual speakers in multi-person streams
|
|
268
|
-
- **Real-time translation** โ Translate to any supported language with sub-second latency
|
|
269
|
-
- **Partial + final results** โ Get streaming partial transcripts and confirmed final text
|
|
270
|
-
- **Credit-based billing** โ 1 credit = 1 minute of transcription/translation
|
|
271
233
|
|
|
272
|
-
|
|
234
|
+
## Live Captions (Speech-to-Text)
|
|
235
|
+
|
|
236
|
+
Transcribe and translate any TikTok LIVE stream in real-time. **This feature is unique to TikTool Live โ no other TikTok library offers it.**
|
|
273
237
|
|
|
274
238
|
```typescript
|
|
275
|
-
import { TikTokCaptions } from '
|
|
276
|
-
|
|
277
|
-
const captions = new TikTokCaptions({
|
|
278
|
-
uniqueId: 'streamer_name',
|
|
279
|
-
apiKey: 'YOUR_API_KEY',
|
|
280
|
-
translate: 'en',
|
|
281
|
-
diarization: true,
|
|
282
|
-
=======
|
|
239
|
+
import { TikTokCaptions } from 'tiktok-live-api';
|
|
240
|
+
|
|
283
241
|
const captions = new TikTokCaptions('streamer_username', {
|
|
284
242
|
apiKey: 'YOUR_API_KEY',
|
|
285
243
|
translate: 'en', // translate to English
|
|
286
244
|
diarization: true, // identify who is speaking
|
|
287
|
-
>>>>>>> ac7990ac0be77c4206d9d8fa0bccbe1c85a2bbe6
|
|
288
245
|
});
|
|
289
246
|
|
|
290
247
|
captions.on('caption', (event) => {
|
|
@@ -424,9 +381,16 @@ client.on('gift', (event: GiftEvent) => {
|
|
|
424
381
|
| Tier | Requests/Day | WebSocket Connections | Price |
|
|
425
382
|
|------|-------------|----------------------|-------|
|
|
426
383
|
| Sandbox | 50 | 1 (5 min) | Free |
|
|
427
|
-
| Basic | 10,000 | 3 (8h) | $
|
|
428
|
-
| Pro | 75,000 | 50 (8h) | $
|
|
429
|
-
| Ultra | 300,000 |
|
|
384
|
+
| Basic | 10,000 | 3 (8h) | $9/wk +tax |
|
|
385
|
+
| Pro | 75,000 | 50 (8h) | $19/wk +tax |
|
|
386
|
+
| Ultra | 300,000 | 250 (8h) | $58/wk +tax |
|
|
387
|
+
|
|
388
|
+
Full plan details at [tik.tools/pricing](https://tik.tools/pricing). Highlights:
|
|
389
|
+
|
|
390
|
+
- **Sandbox** ($0): 50 req/day ยท 1 WS (5 min limit) ยท basic endpoints ยท signatures included
|
|
391
|
+
- **Basic** ($9/wk): 10K req/day ยท 3 WS ยท all endpoints ยท 1 AI caption stream
|
|
392
|
+
- **Pro** ($19/wk, **most popular**): 75K req/day ยท 50 WS ยท all endpoints ยท 5 AI caption streams ยท priority routing ยท chat support
|
|
393
|
+
- **Ultra** ($58/wk): 300K req/day ยท 250 WS ยท 20 AI caption streams ยท **League Rankings API** (Ultra only) ยท 99.5% uptime SLA ยท priority chat support
|
|
430
394
|
|
|
431
395
|
## Also Available
|
|
432
396
|
|
package/dist/index.d.mts
CHANGED
|
@@ -59,12 +59,70 @@ interface RoomUserSeqEvent {
|
|
|
59
59
|
viewerCount: number;
|
|
60
60
|
topViewers: TikTokUser[];
|
|
61
61
|
}
|
|
62
|
-
/** Payload for `battle` events. */
|
|
62
|
+
/** Payload for `battle` events (PK start / end / countdown). */
|
|
63
63
|
interface BattleEvent {
|
|
64
64
|
type: string;
|
|
65
|
+
battleId: string;
|
|
66
|
+
/** 1=ACTIVE, 2=STARTING, 3=ENDED, 4=PREPARING */
|
|
67
|
+
status: number;
|
|
68
|
+
battleDuration: number;
|
|
65
69
|
teams: Array<Record<string, unknown>>;
|
|
66
70
|
scores: number[];
|
|
67
71
|
}
|
|
72
|
+
/** One host on a battle side โ present per-host in multi-guest PK. */
|
|
73
|
+
interface BattleHost {
|
|
74
|
+
hostUserId: string;
|
|
75
|
+
teamTotalScore: number;
|
|
76
|
+
teamIdx: number;
|
|
77
|
+
/** Sorted MVP first (highest score โ lowest). */
|
|
78
|
+
contributors: BattleContributor[];
|
|
79
|
+
}
|
|
80
|
+
interface BattleContributor {
|
|
81
|
+
userId: string;
|
|
82
|
+
score: number;
|
|
83
|
+
nickname: string;
|
|
84
|
+
}
|
|
85
|
+
/** Payload for `battleArmies` events โ score updates during PK. */
|
|
86
|
+
interface BattleArmiesEvent {
|
|
87
|
+
battleId: string;
|
|
88
|
+
/** 1=ACTIVE, 2=STARTING, 3=ENDED, 4=PREPARING */
|
|
89
|
+
status: number;
|
|
90
|
+
teams: Array<Record<string, unknown>>;
|
|
91
|
+
matchId?: string;
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
startedAtMs?: number;
|
|
94
|
+
serverTsMs?: number;
|
|
95
|
+
sessionTag?: string;
|
|
96
|
+
durationSec?: number;
|
|
97
|
+
secsRemaining?: number;
|
|
98
|
+
/** Per-host breakdown with MVP contributors. */
|
|
99
|
+
hosts?: BattleHost[];
|
|
100
|
+
}
|
|
101
|
+
/** Payload for `battleItemCard` events โ multipliers (x2/x3), gloves, mist, etc. */
|
|
102
|
+
interface BattleItemCardEvent {
|
|
103
|
+
battleId: string;
|
|
104
|
+
cardType: number;
|
|
105
|
+
/** 'gloves' | 'mist' | 'booster_x2' | 'booster_x3' | 'match_guide' |
|
|
106
|
+
* 'thunder' | 'extra_time' | raw resource key. */
|
|
107
|
+
effect: string;
|
|
108
|
+
effectKey: string;
|
|
109
|
+
/** 2 or 3 for booster_x2/x3, otherwise 0. */
|
|
110
|
+
multiplier: number;
|
|
111
|
+
senderUserId: string;
|
|
112
|
+
senderNickname: string;
|
|
113
|
+
senderUniqueId: string;
|
|
114
|
+
senderAvatarUrl: string;
|
|
115
|
+
activatedAtSec: number;
|
|
116
|
+
durationSec: number;
|
|
117
|
+
endsAtSec: number;
|
|
118
|
+
commentTemplate: string;
|
|
119
|
+
/** Full TikTok CDN URL for the card art (webp/jpeg). */
|
|
120
|
+
iconUrl: string;
|
|
121
|
+
/** Short identifier e.g. 'card_mist_v3' / 'card_crit_v3' / 'top3_buffer'. */
|
|
122
|
+
iconKey: string;
|
|
123
|
+
/** Hex e.g. '#BCD9E0' (mist blue), '#E0D4BC' (gloves tan). */
|
|
124
|
+
accentColor: string;
|
|
125
|
+
}
|
|
68
126
|
/** Payload for `roomPin` (starred/pinned message) events. */
|
|
69
127
|
interface RoomPinEvent {
|
|
70
128
|
/** User who wrote the pinned message. */
|
|
@@ -128,6 +186,8 @@ interface TikTokLiveEventMap {
|
|
|
128
186
|
subscribe: SocialEvent;
|
|
129
187
|
roomUserSeq: RoomUserSeqEvent;
|
|
130
188
|
battle: BattleEvent;
|
|
189
|
+
battleArmies: BattleArmiesEvent;
|
|
190
|
+
battleItemCard: BattleItemCardEvent;
|
|
131
191
|
roomPin: RoomPinEvent;
|
|
132
192
|
envelope: Record<string, unknown>;
|
|
133
193
|
streamEnd: Record<string, unknown>;
|
package/dist/index.d.ts
CHANGED
|
@@ -59,12 +59,70 @@ interface RoomUserSeqEvent {
|
|
|
59
59
|
viewerCount: number;
|
|
60
60
|
topViewers: TikTokUser[];
|
|
61
61
|
}
|
|
62
|
-
/** Payload for `battle` events. */
|
|
62
|
+
/** Payload for `battle` events (PK start / end / countdown). */
|
|
63
63
|
interface BattleEvent {
|
|
64
64
|
type: string;
|
|
65
|
+
battleId: string;
|
|
66
|
+
/** 1=ACTIVE, 2=STARTING, 3=ENDED, 4=PREPARING */
|
|
67
|
+
status: number;
|
|
68
|
+
battleDuration: number;
|
|
65
69
|
teams: Array<Record<string, unknown>>;
|
|
66
70
|
scores: number[];
|
|
67
71
|
}
|
|
72
|
+
/** One host on a battle side โ present per-host in multi-guest PK. */
|
|
73
|
+
interface BattleHost {
|
|
74
|
+
hostUserId: string;
|
|
75
|
+
teamTotalScore: number;
|
|
76
|
+
teamIdx: number;
|
|
77
|
+
/** Sorted MVP first (highest score โ lowest). */
|
|
78
|
+
contributors: BattleContributor[];
|
|
79
|
+
}
|
|
80
|
+
interface BattleContributor {
|
|
81
|
+
userId: string;
|
|
82
|
+
score: number;
|
|
83
|
+
nickname: string;
|
|
84
|
+
}
|
|
85
|
+
/** Payload for `battleArmies` events โ score updates during PK. */
|
|
86
|
+
interface BattleArmiesEvent {
|
|
87
|
+
battleId: string;
|
|
88
|
+
/** 1=ACTIVE, 2=STARTING, 3=ENDED, 4=PREPARING */
|
|
89
|
+
status: number;
|
|
90
|
+
teams: Array<Record<string, unknown>>;
|
|
91
|
+
matchId?: string;
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
startedAtMs?: number;
|
|
94
|
+
serverTsMs?: number;
|
|
95
|
+
sessionTag?: string;
|
|
96
|
+
durationSec?: number;
|
|
97
|
+
secsRemaining?: number;
|
|
98
|
+
/** Per-host breakdown with MVP contributors. */
|
|
99
|
+
hosts?: BattleHost[];
|
|
100
|
+
}
|
|
101
|
+
/** Payload for `battleItemCard` events โ multipliers (x2/x3), gloves, mist, etc. */
|
|
102
|
+
interface BattleItemCardEvent {
|
|
103
|
+
battleId: string;
|
|
104
|
+
cardType: number;
|
|
105
|
+
/** 'gloves' | 'mist' | 'booster_x2' | 'booster_x3' | 'match_guide' |
|
|
106
|
+
* 'thunder' | 'extra_time' | raw resource key. */
|
|
107
|
+
effect: string;
|
|
108
|
+
effectKey: string;
|
|
109
|
+
/** 2 or 3 for booster_x2/x3, otherwise 0. */
|
|
110
|
+
multiplier: number;
|
|
111
|
+
senderUserId: string;
|
|
112
|
+
senderNickname: string;
|
|
113
|
+
senderUniqueId: string;
|
|
114
|
+
senderAvatarUrl: string;
|
|
115
|
+
activatedAtSec: number;
|
|
116
|
+
durationSec: number;
|
|
117
|
+
endsAtSec: number;
|
|
118
|
+
commentTemplate: string;
|
|
119
|
+
/** Full TikTok CDN URL for the card art (webp/jpeg). */
|
|
120
|
+
iconUrl: string;
|
|
121
|
+
/** Short identifier e.g. 'card_mist_v3' / 'card_crit_v3' / 'top3_buffer'. */
|
|
122
|
+
iconKey: string;
|
|
123
|
+
/** Hex e.g. '#BCD9E0' (mist blue), '#E0D4BC' (gloves tan). */
|
|
124
|
+
accentColor: string;
|
|
125
|
+
}
|
|
68
126
|
/** Payload for `roomPin` (starred/pinned message) events. */
|
|
69
127
|
interface RoomPinEvent {
|
|
70
128
|
/** User who wrote the pinned message. */
|
|
@@ -128,6 +186,8 @@ interface TikTokLiveEventMap {
|
|
|
128
186
|
subscribe: SocialEvent;
|
|
129
187
|
roomUserSeq: RoomUserSeqEvent;
|
|
130
188
|
battle: BattleEvent;
|
|
189
|
+
battleArmies: BattleArmiesEvent;
|
|
190
|
+
battleItemCard: BattleItemCardEvent;
|
|
131
191
|
roomPin: RoomPinEvent;
|
|
132
192
|
envelope: Record<string, unknown>;
|
|
133
193
|
streamEnd: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiktok-live-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Unofficial TikTok LIVE API Client โ Real-time chat, gifts, viewers, battles, and AI live captions from any TikTok livestream. Managed WebSocket API with 99.9% uptime.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -12,12 +12,8 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"bin": {
|
|
16
|
-
"tiktok-live-api": "./bin/demo.mjs"
|
|
17
|
-
},
|
|
18
15
|
"files": [
|
|
19
16
|
"dist",
|
|
20
|
-
"bin",
|
|
21
17
|
"README.md",
|
|
22
18
|
"LICENSE"
|
|
23
19
|
],
|
package/bin/demo.mjs
DELETED
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import WebSocket from 'ws';
|
|
4
|
-
import readline from 'readline';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
|
|
9
|
-
// โโ Boilerplate Components โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
10
|
-
|
|
11
|
-
const REACT_TEMPLATE = [
|
|
12
|
-
'"use client";',
|
|
13
|
-
"import React, { useEffect, useState, useRef } from 'react';",
|
|
14
|
-
"",
|
|
15
|
-
"export default function TikTokLiveComponent() {",
|
|
16
|
-
" const [events, setEvents] = useState([]);",
|
|
17
|
-
" const [inputUsername, setInputUsername] = useState('gbnews');",
|
|
18
|
-
" const [apiKey, setApiKey] = useState('demo_tiktokliveapi_public_2026');",
|
|
19
|
-
" const [connected, setConnected] = useState(false);",
|
|
20
|
-
" const wsRef = useRef(null);",
|
|
21
|
-
"",
|
|
22
|
-
" function addEvent(str) {",
|
|
23
|
-
" setEvents(prev => [...prev.slice(-50), str]);",
|
|
24
|
-
" }",
|
|
25
|
-
"",
|
|
26
|
-
" function connectStream(e) {",
|
|
27
|
-
" if (e) e.preventDefault();",
|
|
28
|
-
" if (wsRef.current) { wsRef.current.close(); wsRef.current = null; }",
|
|
29
|
-
" setEvents([]);",
|
|
30
|
-
" setConnected(false);",
|
|
31
|
-
" const user = (inputUsername || '').replace(/^@/, '');",
|
|
32
|
-
" if (!user) return;",
|
|
33
|
-
" try {",
|
|
34
|
-
" const ws = new WebSocket('wss://api.tik.tools?uniqueId=' + user + '&apiKey=' + apiKey);",
|
|
35
|
-
" wsRef.current = ws;",
|
|
36
|
-
" ws.onopen = () => { setConnected(true); addEvent('Connected to @' + user); };",
|
|
37
|
-
" ws.onclose = () => { setConnected(false); wsRef.current = null; };",
|
|
38
|
-
" ws.onerror = () => { addEvent('WebSocket error'); };",
|
|
39
|
-
" ws.onmessage = (evt) => {",
|
|
40
|
-
" try {",
|
|
41
|
-
" const msg = JSON.parse(evt.data);",
|
|
42
|
-
" const t = msg.event || '';",
|
|
43
|
-
" const d = msg.data || {};",
|
|
44
|
-
" const u = d.user ? d.user.uniqueId : '';",
|
|
45
|
-
" if (t === 'chat') addEvent('[chat] ' + u + ': ' + d.comment);",
|
|
46
|
-
" else if (t === 'gift') addEvent('[gift] ' + u + ' sent ' + (d.giftName || 'a gift'));",
|
|
47
|
-
" else if (t === 'like') addEvent('[like] ' + u + ' liked');",
|
|
48
|
-
" else if (t === 'member') addEvent('[join] ' + u + ' joined');",
|
|
49
|
-
" else if (t === 'roomUserSeq') addEvent('[viewers] ' + (d.viewerCount || 0));",
|
|
50
|
-
" } catch (err) {}",
|
|
51
|
-
" };",
|
|
52
|
-
" } catch (err) { addEvent('Connection failed: ' + err.message); }",
|
|
53
|
-
" }",
|
|
54
|
-
"",
|
|
55
|
-
" useEffect(() => { return () => { if (wsRef.current) wsRef.current.close(); }; }, []);",
|
|
56
|
-
"",
|
|
57
|
-
" return (",
|
|
58
|
-
" <div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 600, margin: '40px auto', border: '1px solid #e5e7eb', borderRadius: '12px', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', background: '#fff' }}>",
|
|
59
|
-
" <div style={{ padding: '24px', borderBottom: '1px solid #e5e7eb' }}>",
|
|
60
|
-
" <h2 style={{ margin: '0 0 8px 0', fontSize: '1.5rem', fontWeight: 700, color: '#111827' }}>TikTok Live SDK</h2>",
|
|
61
|
-
" <p style={{ margin: 0, fontSize: '0.875rem', color: '#4b5563' }}>",
|
|
62
|
-
' Get your own API key at <a href="https://tik.tools" target="_blank" style={{ color: \'#3b82f6\', textDecoration: \'none\' }}>tik.tools</a>',
|
|
63
|
-
" </p>",
|
|
64
|
-
" </div>",
|
|
65
|
-
" <div style={{ padding: '24px' }}>",
|
|
66
|
-
" <form onSubmit={connectStream} style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '24px' }}>",
|
|
67
|
-
" <div>",
|
|
68
|
-
" <label style={{ display: 'block', fontSize: '0.875rem', fontWeight: 600, marginBottom: '6px', color: '#111827' }}>TikTok Username</label>",
|
|
69
|
-
' <input type="text" value={inputUsername} onChange={(e) => setInputUsername(e.target.value)} style={{ width: \'100%\', padding: \'8px 12px\', borderRadius: \'6px\', border: \'1px solid #d1d5db\', boxSizing: \'border-box\', color: \'#111827\' }} placeholder="e.g. gbnews" />',
|
|
70
|
-
" </div>",
|
|
71
|
-
" <div>",
|
|
72
|
-
" <label style={{ display: 'block', fontSize: '0.875rem', fontWeight: 600, marginBottom: '6px', color: '#111827' }}>API Key</label>",
|
|
73
|
-
' <input type="text" value={apiKey} onChange={(e) => setApiKey(e.target.value)} style={{ width: \'100%\', padding: \'8px 12px\', borderRadius: \'6px\', border: \'1px solid #d1d5db\', boxSizing: \'border-box\', fontFamily: \'monospace\', color: \'#111827\' }} />',
|
|
74
|
-
" <p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#6b7280' }}>The demo key is rate-limited. Replace with your own key for production.</p>",
|
|
75
|
-
" </div>",
|
|
76
|
-
' <button type="submit" style={{ background: \'#111827\', color: \'#fff\', padding: \'10px 16px\', borderRadius: \'6px\', border: \'none\', fontWeight: 600, cursor: \'pointer\', fontSize: \'0.875rem\' }}>Connect Stream</button>',
|
|
77
|
-
" </form>",
|
|
78
|
-
" <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>",
|
|
79
|
-
" <div style={{ width: 10, height: 10, borderRadius: '50%', background: connected ? '#10b981' : '#ef4444', transition: 'background 0.3s' }}></div>",
|
|
80
|
-
" <span style={{ fontSize: '0.875rem', color: '#111827', fontWeight: 600 }}>{connected ? 'Connected' : 'Disconnected'}</span>",
|
|
81
|
-
" </div>",
|
|
82
|
-
" <div style={{ height: 350, overflowY: 'auto', background: '#f9fafb', padding: '12px', borderRadius: '8px', border: '1px solid #e5e7eb', fontSize: '0.875rem', display: 'flex', flexDirection: 'column' }}>",
|
|
83
|
-
" {events.map((e, i) => <div key={i} style={{ padding: '6px 0', borderBottom: '1px solid #f3f4f6', wordBreak: 'break-word', color: '#374151' }}>{e}</div>)}",
|
|
84
|
-
" {events.length === 0 && <div style={{ color: '#9ca3af', textAlign: 'center', margin: 'auto 0' }}>{connected ? 'Waiting for events...' : 'Connect to see events'}</div>}",
|
|
85
|
-
" </div>",
|
|
86
|
-
" </div>",
|
|
87
|
-
" </div>",
|
|
88
|
-
" );",
|
|
89
|
-
"}",
|
|
90
|
-
].join('\n') + "\n";
|
|
91
|
-
|
|
92
|
-
const VUE_TEMPLATE = [
|
|
93
|
-
"<template>",
|
|
94
|
-
' <div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); background: #fff">',
|
|
95
|
-
' <div style="padding: 24px; border-bottom: 1px solid #e5e7eb">',
|
|
96
|
-
' <h2 style="margin: 0 0 8px 0; font-size: 1.5rem; font-weight: 600">TikTok Live SDK</h2>',
|
|
97
|
-
' <p style="margin: 0; font-size: 0.875rem; color: #6b7280">',
|
|
98
|
-
' Get your own API key and unlock unlimited connections at <a href="https://tik.tools" target="_blank" style="color: #3b82f6; text-decoration: none">tik.tools</a>',
|
|
99
|
-
" </p>",
|
|
100
|
-
" </div>",
|
|
101
|
-
' <div style="padding: 24px">',
|
|
102
|
-
' <form @submit.prevent="handleConnect" style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px">',
|
|
103
|
-
" <div>",
|
|
104
|
-
' <label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 6px">TikTok Username</label>',
|
|
105
|
-
' <input type="text" v-model="inputUsername" style="width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid #d1d5db; box-sizing: border-box" placeholder="e.g. gbnews" />',
|
|
106
|
-
" </div>",
|
|
107
|
-
" <div>",
|
|
108
|
-
' <label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 6px">API Key</label>',
|
|
109
|
-
' <input type="text" v-model="apiKey" style="width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid #d1d5db; box-sizing: border-box; font-family: monospace" />',
|
|
110
|
-
' <p style="margin: 4px 0 0 0; font-size: 0.75rem; color: #9ca3af">The demo key is rate-limited. Replace with your own key for production.</p>',
|
|
111
|
-
" </div>",
|
|
112
|
-
' <button type="submit" style="background: #000; color: #fff; padding: 10px 16px; border-radius: 6px; border: none; font-weight: 500; cursor: pointer">Connect Stream</button>',
|
|
113
|
-
" </form>",
|
|
114
|
-
' <div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px">',
|
|
115
|
-
" <div :style=\"{ width: '10px', height: '10px', borderRadius: '50%', background: connected ? '#10b981' : '#ef4444', transition: 'background 0.3s' }\"></div>",
|
|
116
|
-
" <span style=\"font-size: 0.875rem; color: '#374151'; font-weight: 500\">{{ connected ? 'Connected to @' + activeUsername : 'Disconnected' }}</span>",
|
|
117
|
-
" </div>",
|
|
118
|
-
' <div style="height: 350px; overflow-y: auto; background: #f9fafb; padding: 12px; border-radius: 8px; border: 1px solid #e5e7eb; font-size: 0.875rem; display: flex; flex-direction: column">',
|
|
119
|
-
' <div v-for="(e, i) in events" :key="i" style="padding: 6px 0; border-bottom: 1px solid #f3f4f6; word-break: break-word">',
|
|
120
|
-
" {{ e }}",
|
|
121
|
-
" </div>",
|
|
122
|
-
' <div v-if="events.length === 0" style="color: #9ca3af; text-align: center; margin: auto 0">{{ connected ? "Waiting for events..." : "Connect to a valid stream to see events" }}</div>',
|
|
123
|
-
" </div>",
|
|
124
|
-
" </div>",
|
|
125
|
-
" </div>",
|
|
126
|
-
"</template>",
|
|
127
|
-
"",
|
|
128
|
-
"<script setup>",
|
|
129
|
-
"import { ref, onMounted, onUnmounted } from 'vue'",
|
|
130
|
-
"",
|
|
131
|
-
"const inputUsername = ref('gbnews')",
|
|
132
|
-
"const activeUsername = ref('gbnews')",
|
|
133
|
-
"const apiKey = ref('demo_tiktokliveapi_public_2026')",
|
|
134
|
-
"const connected = ref(false)",
|
|
135
|
-
"const events = ref([])",
|
|
136
|
-
"let ws = null",
|
|
137
|
-
"",
|
|
138
|
-
"function addEvent(str) {",
|
|
139
|
-
" events.value.push(str)",
|
|
140
|
-
" if (events.value.length > 50) events.value.shift()",
|
|
141
|
-
"}",
|
|
142
|
-
"",
|
|
143
|
-
"function connectStream() {",
|
|
144
|
-
" if (ws) { ws.close(); ws = null }",
|
|
145
|
-
" events.value = []",
|
|
146
|
-
" connected.value = false",
|
|
147
|
-
" activeUsername.value = inputUsername.value",
|
|
148
|
-
" const user = (activeUsername.value || '').replace(/^@/, '')",
|
|
149
|
-
" if (!user) return",
|
|
150
|
-
" const key = apiKey.value || ''",
|
|
151
|
-
" try {",
|
|
152
|
-
" ws = new WebSocket('wss://api.tik.tools?uniqueId=' + user + '&apiKey=' + key)",
|
|
153
|
-
" } catch (e) { addEvent('โ ๏ธ Connection failed: ' + e.message); return }",
|
|
154
|
-
" ws.onopen = () => { connected.value = true; addEvent('โ
Connected to @' + user) }",
|
|
155
|
-
" ws.onclose = () => { connected.value = false; ws = null; addEvent('โ Disconnected') }",
|
|
156
|
-
" ws.onerror = () => { addEvent('โ ๏ธ WebSocket error') }",
|
|
157
|
-
" ws.onmessage = (evt) => {",
|
|
158
|
-
" try {",
|
|
159
|
-
" const msg = JSON.parse(evt.data)",
|
|
160
|
-
" const t = msg.event || ''",
|
|
161
|
-
" const d = msg.data || {}",
|
|
162
|
-
" const u = d.user ? d.user.uniqueId : ''",
|
|
163
|
-
" if (t === 'chat') addEvent('๐ฌ ' + u + ': ' + d.comment)",
|
|
164
|
-
" else if (t === 'gift') addEvent('๐ ' + u + ' sent ' + (d.giftName || 'a gift'))",
|
|
165
|
-
" else if (t === 'like') addEvent('โค๏ธ ' + u + ' liked')",
|
|
166
|
-
" else if (t === 'member') addEvent('๐ ' + u + ' joined')",
|
|
167
|
-
" } catch (e) {}",
|
|
168
|
-
" }",
|
|
169
|
-
"}",
|
|
170
|
-
"",
|
|
171
|
-
"const handleConnect = () => { connectStream() }",
|
|
172
|
-
"",
|
|
173
|
-
"onMounted(() => { connectStream() })",
|
|
174
|
-
"onUnmounted(() => { if (ws) ws.close() })",
|
|
175
|
-
"</script>"
|
|
176
|
-
].join('\n') + "\n";
|
|
177
|
-
|
|
178
|
-
const E = String.fromCharCode(27);
|
|
179
|
-
const R = E + "[0m";
|
|
180
|
-
const B = E + "[1m";
|
|
181
|
-
const D = E + "[2m";
|
|
182
|
-
const C = {
|
|
183
|
-
cyan: E + "[38;5;80m", green: E + "[38;5;114m", yellow: E + "[38;5;222m",
|
|
184
|
-
mag: E + "[38;5;176m", blue: E + "[38;5;111m", red: E + "[38;5;203m",
|
|
185
|
-
gray: E + "[38;5;242m", white: E + "[38;5;252m", pink: E + "[38;5;218m",
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// โโ Interactive Prompt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
189
|
-
|
|
190
|
-
const args = process.argv.slice(2);
|
|
191
|
-
const isInteractive = args.length === 0;
|
|
192
|
-
|
|
193
|
-
if (!isInteractive) {
|
|
194
|
-
runDemo();
|
|
195
|
-
} else {
|
|
196
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
197
|
-
|
|
198
|
-
console.log("\n " + B + "tiktok-live-api" + R + "\n Unofficial TikTok LIVE SDK by TikTool\n");
|
|
199
|
-
console.log(" What would you like to do?");
|
|
200
|
-
console.log(" 1) " + C.cyan + "Watch Live Terminal Demo" + R);
|
|
201
|
-
console.log(" 2) " + C.green + "Scaffold a Nuxt 3 (Vue) App" + R);
|
|
202
|
-
console.log(" 3) " + C.blue + "Scaffold a Next.js (React) App" + R);
|
|
203
|
-
console.log(" 4) " + C.yellow + "Scaffold a Vite (React) App" + R);
|
|
204
|
-
console.log(" 5) " + C.mag + "Scaffold a Plain Node.js Project" + R);
|
|
205
|
-
console.log(" 6) " + C.pink + "Scaffold a Python Project" + R + "\n");
|
|
206
|
-
|
|
207
|
-
function promptAction() {
|
|
208
|
-
rl.question(" Choose [1-6]: ", (answer) => {
|
|
209
|
-
handleMenu(answer.trim());
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function handleMenu(choice) {
|
|
214
|
-
if (choice === '1') {
|
|
215
|
-
rl.close();
|
|
216
|
-
return runDemo();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!['2', '3', '4', '5', '6'].includes(choice)) {
|
|
220
|
-
console.log(" " + C.red + "Invalid choice." + R);
|
|
221
|
-
return promptAction();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
rl.question("\n Target directory (e.g. ./my-app) [./tiktok-live-project]: ", (dir) => {
|
|
225
|
-
dir = dir.trim() || './tiktok-live-project';
|
|
226
|
-
|
|
227
|
-
if (choice === '6') {
|
|
228
|
-
rl.close();
|
|
229
|
-
return runScaffold(choice, dir, 'pip');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.log("\n Select Package Manager:");
|
|
233
|
-
console.log(" 1) npm " + D + "(Default)" + R);
|
|
234
|
-
console.log(" 2) yarn");
|
|
235
|
-
console.log(" 3) pnpm");
|
|
236
|
-
console.log(" 4) bun");
|
|
237
|
-
|
|
238
|
-
rl.question("\n Choose [1-4]: ", (pmAnswer) => {
|
|
239
|
-
const pmChoice = pmAnswer.trim() || '1';
|
|
240
|
-
const pmMap = { '1': 'npm', '2': 'yarn', '3': 'pnpm', '4': 'bun' };
|
|
241
|
-
const pm = pmMap[pmChoice] || 'npm';
|
|
242
|
-
|
|
243
|
-
rl.close();
|
|
244
|
-
runScaffold(choice, dir, pm);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
promptAction();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// โโ Scaffolding Logic โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
253
|
-
|
|
254
|
-
function runScaffold(choice, targetDir, pm) {
|
|
255
|
-
const isNuxt = choice === '2';
|
|
256
|
-
const isNext = choice === '3';
|
|
257
|
-
const isVite = choice === '4';
|
|
258
|
-
const isNode = choice === '5';
|
|
259
|
-
const isPython = choice === '6';
|
|
260
|
-
const fullPath = path.resolve(process.cwd(), targetDir);
|
|
261
|
-
|
|
262
|
-
const npxCmd = pm === 'bun' ? 'bunx' : (pm === 'pnpm' ? 'pnpm dlx' : 'npx');
|
|
263
|
-
const installCmd = pm === 'npm' ? 'npm install tiktok-live-api' : pm === 'yarn' ? 'yarn add tiktok-live-api' : pm === 'pnpm' ? 'pnpm add tiktok-live-api' : 'bun add tiktok-live-api';
|
|
264
|
-
const devCmd = pm === 'npm' ? 'npm run dev' : pm === 'yarn' ? 'yarn dev' : pm === 'pnpm' ? 'pnpm dev' : 'bun run dev';
|
|
265
|
-
|
|
266
|
-
console.log("\n " + B + "๐ Scaffolding project into " + targetDir + " using " + pm + "..." + R + "\n");
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
if (isNode) {
|
|
270
|
-
if (!fs.existsSync(fullPath)) fs.mkdirSync(fullPath, { recursive: true });
|
|
271
|
-
} else if (isPython) {
|
|
272
|
-
if (!fs.existsSync(fullPath)) fs.mkdirSync(fullPath, { recursive: true });
|
|
273
|
-
} else if (isNuxt) {
|
|
274
|
-
execSync(npxCmd + ' nuxi@latest init "' + targetDir + '" --packageManager ' + pm, { stdio: 'inherit' });
|
|
275
|
-
} else if (isNext) {
|
|
276
|
-
execSync(npxCmd + ' create-next-app@latest "' + targetDir + '" --js --eslint --tailwind --app --src-dir --import-alias "@/*" --use-' + pm, { stdio: 'inherit' });
|
|
277
|
-
} else if (isVite) {
|
|
278
|
-
execSync(npxCmd + ' create-vite@latest "' + targetDir + '" --no-interactive --template react', { stdio: 'inherit' });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (isNode) {
|
|
282
|
-
// โโ Plain Node.js Starter Kit โโ
|
|
283
|
-
const pkgJson = {
|
|
284
|
-
name: path.basename(fullPath),
|
|
285
|
-
version: '1.0.0',
|
|
286
|
-
type: 'module',
|
|
287
|
-
scripts: { start: 'node index.mjs', dev: 'node --watch index.mjs' },
|
|
288
|
-
dependencies: {}
|
|
289
|
-
};
|
|
290
|
-
fs.writeFileSync(path.join(fullPath, 'package.json'), JSON.stringify(pkgJson, null, 2) + '\n');
|
|
291
|
-
|
|
292
|
-
console.log("\n " + B + "๐ฆ Installing " + C.cyan + "tiktok-live-api" + B + " SDK..." + R + "\n");
|
|
293
|
-
execSync(installCmd, { cwd: fullPath, stdio: 'inherit' });
|
|
294
|
-
|
|
295
|
-
fs.writeFileSync(path.join(fullPath, 'index.mjs'), [
|
|
296
|
-
"import { TikTokLive } from 'tiktok-live-api';",
|
|
297
|
-
"",
|
|
298
|
-
"// โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ",
|
|
299
|
-
"const USERNAME = 'gbnews'; // TikTok @username",
|
|
300
|
-
"const API_KEY = 'demo_tiktokliveapi_public_2026'; // Get yours at https://tik.tools",
|
|
301
|
-
"",
|
|
302
|
-
"// โโ Connect โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ",
|
|
303
|
-
"const client = new TikTokLive(USERNAME, { apiKey: API_KEY });",
|
|
304
|
-
"",
|
|
305
|
-
"client.on('connected', () => {",
|
|
306
|
-
" console.log('\\nโ
Connected to @' + USERNAME + '\\n');",
|
|
307
|
-
"});",
|
|
308
|
-
"",
|
|
309
|
-
"client.on('chat', (data) => {",
|
|
310
|
-
" console.log(`๐ฌ ${data.user.uniqueId}: ${data.comment}`);",
|
|
311
|
-
"});",
|
|
312
|
-
"",
|
|
313
|
-
"client.on('gift', (data) => {",
|
|
314
|
-
" console.log(`๐ ${data.user.uniqueId} sent ${data.giftName} x${data.repeatCount}`);",
|
|
315
|
-
"});",
|
|
316
|
-
"",
|
|
317
|
-
"client.on('like', (data) => {",
|
|
318
|
-
" console.log(`โค๏ธ ${data.user.uniqueId} liked (total: ${data.totalLikeCount})`);",
|
|
319
|
-
"});",
|
|
320
|
-
"",
|
|
321
|
-
"client.on('member', (data) => {",
|
|
322
|
-
" console.log(`๐ ${data.user.uniqueId} joined`);",
|
|
323
|
-
"});",
|
|
324
|
-
"",
|
|
325
|
-
"client.on('roomUserSeq', (data) => {",
|
|
326
|
-
" console.log(`๐ Viewers: ${data.viewerCount}`);",
|
|
327
|
-
"});",
|
|
328
|
-
"",
|
|
329
|
-
"client.on('disconnected', () => {",
|
|
330
|
-
" console.log('\\nโ Disconnected\\n');",
|
|
331
|
-
"});",
|
|
332
|
-
"",
|
|
333
|
-
"client.on('error', (err) => {",
|
|
334
|
-
" console.error('โ ๏ธ Error:', err.message);",
|
|
335
|
-
"});",
|
|
336
|
-
"",
|
|
337
|
-
"console.log('๐ Connecting to @' + USERNAME + '...');",
|
|
338
|
-
"client.connect().catch((err) => {",
|
|
339
|
-
" console.error('Failed to connect:', err.message);",
|
|
340
|
-
" process.exit(1);",
|
|
341
|
-
"});",
|
|
342
|
-
].join('\n') + '\n');
|
|
343
|
-
|
|
344
|
-
fs.writeFileSync(path.join(fullPath, 'README.md'), [
|
|
345
|
-
'# TikTok Live - Node.js Starter',
|
|
346
|
-
'',
|
|
347
|
-
'A ready-to-run Node.js project connecting to TikTok LIVE streams.',
|
|
348
|
-
'',
|
|
349
|
-
'## Quick Start',
|
|
350
|
-
'',
|
|
351
|
-
'```bash',
|
|
352
|
-
'node index.mjs',
|
|
353
|
-
'```',
|
|
354
|
-
'',
|
|
355
|
-
'## Configuration',
|
|
356
|
-
'',
|
|
357
|
-
'Edit `index.mjs` to change:',
|
|
358
|
-
'- `USERNAME` โ the TikTok @username to watch',
|
|
359
|
-
'- `API_KEY` โ get your own at [tik.tools](https://tik.tools)',
|
|
360
|
-
'',
|
|
361
|
-
'## Events',
|
|
362
|
-
'',
|
|
363
|
-
'The demo listens for: `chat`, `gift`, `like`, `member`, `roomUserSeq`.',
|
|
364
|
-
'See the [full docs](https://www.npmjs.com/package/tiktok-live-api) for all available events.',
|
|
365
|
-
'',
|
|
366
|
-
].join('\n'));
|
|
367
|
-
|
|
368
|
-
} else if (isPython) {
|
|
369
|
-
// โโ Python Starter Kit โโ
|
|
370
|
-
fs.writeFileSync(path.join(fullPath, 'main.py'), [
|
|
371
|
-
'"""TikTok Live - Python Starter Kit"""',
|
|
372
|
-
'',
|
|
373
|
-
'import asyncio',
|
|
374
|
-
'import json',
|
|
375
|
-
'import websockets',
|
|
376
|
-
'',
|
|
377
|
-
'# \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500',
|
|
378
|
-
"USERNAME = 'gbnews' # TikTok @username",
|
|
379
|
-
"API_KEY = 'demo_tiktokliveapi_public_2026' # Get yours at https://tik.tools",
|
|
380
|
-
'',
|
|
381
|
-
'',
|
|
382
|
-
'async def main():',
|
|
383
|
-
' """Connect to TikTok LIVE and print events."""',
|
|
384
|
-
' uri = f"wss://api.tik.tools?uniqueId={USERNAME}&apiKey={API_KEY}"',
|
|
385
|
-
' print(f"Connecting to @{USERNAME}...")',
|
|
386
|
-
'',
|
|
387
|
-
' try:',
|
|
388
|
-
' async with websockets.connect(uri) as ws:',
|
|
389
|
-
' print(f"Connected to @{USERNAME}")',
|
|
390
|
-
' print()',
|
|
391
|
-
' async for raw in ws:',
|
|
392
|
-
' try:',
|
|
393
|
-
" msg = json.loads(raw)",
|
|
394
|
-
" event = msg.get('event', '')",
|
|
395
|
-
" data = msg.get('data', {})",
|
|
396
|
-
" user = data.get('user', {}).get('uniqueId', '?')",
|
|
397
|
-
'',
|
|
398
|
-
" if event == 'chat':",
|
|
399
|
-
" comment = data.get('comment', '')",
|
|
400
|
-
' print(f"[chat] {user}: {comment}")',
|
|
401
|
-
" elif event == 'gift':",
|
|
402
|
-
" gift = data.get('giftName', 'a gift')",
|
|
403
|
-
" count = data.get('repeatCount', 1)",
|
|
404
|
-
' print(f"[gift] {user} sent {gift} x{count}")',
|
|
405
|
-
" elif event == 'like':",
|
|
406
|
-
" total = data.get('totalLikeCount', 0)",
|
|
407
|
-
' print(f"[like] {user} liked (total: {total})")',
|
|
408
|
-
" elif event == 'member':",
|
|
409
|
-
' print(f"[join] {user} joined")',
|
|
410
|
-
" elif event == 'roomUserSeq':",
|
|
411
|
-
" viewers = data.get('viewerCount', 0)",
|
|
412
|
-
' print(f"[viewers] {viewers}")',
|
|
413
|
-
' except json.JSONDecodeError:',
|
|
414
|
-
' pass',
|
|
415
|
-
' except KeyboardInterrupt:',
|
|
416
|
-
' print("Disconnected.")',
|
|
417
|
-
' except Exception as e:',
|
|
418
|
-
' print(f"Error: {e}")',
|
|
419
|
-
'',
|
|
420
|
-
'',
|
|
421
|
-
'if __name__ == "__main__":',
|
|
422
|
-
' asyncio.run(main())',
|
|
423
|
-
].join('\n') + '\n');
|
|
424
|
-
|
|
425
|
-
fs.writeFileSync(path.join(fullPath, 'requirements.txt'), 'websockets>=12.0\n');
|
|
426
|
-
|
|
427
|
-
fs.writeFileSync(path.join(fullPath, 'README.md'), [
|
|
428
|
-
'# TikTok Live - Python Starter',
|
|
429
|
-
'',
|
|
430
|
-
'A ready-to-run Python project connecting to TikTok LIVE streams.',
|
|
431
|
-
'',
|
|
432
|
-
'## Quick Start',
|
|
433
|
-
'',
|
|
434
|
-
'```bash',
|
|
435
|
-
'pip install -r requirements.txt',
|
|
436
|
-
'python main.py',
|
|
437
|
-
'```',
|
|
438
|
-
'',
|
|
439
|
-
'## Configuration',
|
|
440
|
-
'',
|
|
441
|
-
'Edit `main.py` to change:',
|
|
442
|
-
'- `USERNAME` โ the TikTok @username to watch',
|
|
443
|
-
'- `API_KEY` โ get your own at [tik.tools](https://tik.tools)',
|
|
444
|
-
'',
|
|
445
|
-
'## Events',
|
|
446
|
-
'',
|
|
447
|
-
'The demo listens for: `chat`, `gift`, `like`, `member`, `roomUserSeq`.',
|
|
448
|
-
'',
|
|
449
|
-
].join('\n'));
|
|
450
|
-
|
|
451
|
-
console.log("\n " + B + "๐ฆ Installing " + C.cyan + "websockets" + B + "..." + R + "\n");
|
|
452
|
-
try { execSync('pip install websockets', { cwd: fullPath, stdio: 'inherit' }); } catch (e) {
|
|
453
|
-
console.log(" " + C.yellow + "Note: run 'pip install -r requirements.txt' manually if pip failed." + R);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
} else if (isNuxt) {
|
|
457
|
-
const isNuxt4 = fs.existsSync(path.join(fullPath, 'app', 'app.vue')) || !fs.existsSync(path.join(fullPath, 'app.vue'));
|
|
458
|
-
const baseApp = isNuxt4 ? path.join(fullPath, 'app') : fullPath;
|
|
459
|
-
|
|
460
|
-
const compDir = path.join(baseApp, 'components');
|
|
461
|
-
if (!fs.existsSync(compDir)) fs.mkdirSync(compDir, { recursive: true });
|
|
462
|
-
fs.writeFileSync(path.join(compDir, 'TikTokLive.vue'), VUE_TEMPLATE);
|
|
463
|
-
|
|
464
|
-
const appVue = path.join(baseApp, 'app.vue');
|
|
465
|
-
fs.writeFileSync(appVue, [
|
|
466
|
-
'<template>',
|
|
467
|
-
' <main style="min-height: 100vh; background: #f9fafb; padding-top: 40px;">',
|
|
468
|
-
' <ClientOnly>',
|
|
469
|
-
' <TikTokLive />',
|
|
470
|
-
' </ClientOnly>',
|
|
471
|
-
' </main>',
|
|
472
|
-
'</template>'
|
|
473
|
-
].join('\n') + '\n');
|
|
474
|
-
} else {
|
|
475
|
-
if (isNext) {
|
|
476
|
-
const compDir = path.join(fullPath, 'src', 'components');
|
|
477
|
-
if (!fs.existsSync(compDir)) fs.mkdirSync(compDir, { recursive: true });
|
|
478
|
-
fs.writeFileSync(path.join(compDir, 'TikTokLive.jsx'), REACT_TEMPLATE);
|
|
479
|
-
|
|
480
|
-
// Write page.js โ create the directory if needed
|
|
481
|
-
const pageDir = path.join(fullPath, 'src', 'app');
|
|
482
|
-
if (!fs.existsSync(pageDir)) fs.mkdirSync(pageDir, { recursive: true });
|
|
483
|
-
fs.writeFileSync(path.join(pageDir, 'page.js'), [
|
|
484
|
-
"import TikTokLive from '@/components/TikTokLive';",
|
|
485
|
-
"",
|
|
486
|
-
"export default function Home() {",
|
|
487
|
-
" return (",
|
|
488
|
-
' <main style={{ minHeight: "100vh", background: "#f9fafb", paddingTop: "40px" }}>',
|
|
489
|
-
" <TikTokLive />",
|
|
490
|
-
" </main>",
|
|
491
|
-
" );",
|
|
492
|
-
"}",
|
|
493
|
-
].join('\n') + '\n');
|
|
494
|
-
} else if (isVite) {
|
|
495
|
-
const srcDir = path.join(fullPath, 'src');
|
|
496
|
-
const compDir = path.join(srcDir, 'components');
|
|
497
|
-
if (!fs.existsSync(compDir)) fs.mkdirSync(compDir, { recursive: true });
|
|
498
|
-
fs.writeFileSync(path.join(compDir, 'TikTokLive.jsx'), REACT_TEMPLATE);
|
|
499
|
-
|
|
500
|
-
// Write App.jsx
|
|
501
|
-
fs.writeFileSync(path.join(srcDir, 'App.jsx'), [
|
|
502
|
-
"import TikTokLive from './components/TikTokLive';",
|
|
503
|
-
"",
|
|
504
|
-
"export default function App() {",
|
|
505
|
-
" return (",
|
|
506
|
-
' <main style={{ minHeight: "100vh", background: "#f9fafb", paddingTop: "40px" }}>',
|
|
507
|
-
" <TikTokLive />",
|
|
508
|
-
" </main>",
|
|
509
|
-
" );",
|
|
510
|
-
"}",
|
|
511
|
-
].join('\n') + '\n');
|
|
512
|
-
|
|
513
|
-
// Write main.jsx entry point
|
|
514
|
-
fs.writeFileSync(path.join(srcDir, 'main.jsx'), [
|
|
515
|
-
"import React from 'react';",
|
|
516
|
-
"import ReactDOM from 'react-dom/client';",
|
|
517
|
-
"import App from './App';",
|
|
518
|
-
"",
|
|
519
|
-
"ReactDOM.createRoot(document.getElementById('root')).render(",
|
|
520
|
-
" <React.StrictMode>",
|
|
521
|
-
" <App />",
|
|
522
|
-
" </React.StrictMode>",
|
|
523
|
-
");",
|
|
524
|
-
].join('\n') + '\n');
|
|
525
|
-
|
|
526
|
-
// Clean up TypeScript files that conflict with our JSX files
|
|
527
|
-
for (const f of ['App.tsx', 'App.ts', 'main.tsx', 'main.ts']) {
|
|
528
|
-
const fp = path.join(srcDir, f);
|
|
529
|
-
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Update index.html to reference main.jsx instead of main.ts/main.tsx
|
|
533
|
-
const indexHtml = path.join(fullPath, 'index.html');
|
|
534
|
-
if (fs.existsSync(indexHtml)) {
|
|
535
|
-
let html = fs.readFileSync(indexHtml, 'utf8');
|
|
536
|
-
html = html.replace(/src=\/src\/main\.tsx?/g, 'src=/src/main.jsx');
|
|
537
|
-
html = html.replace(/src="\/src\/main\.tsx?"/g, 'src="/src/main.jsx"');
|
|
538
|
-
fs.writeFileSync(indexHtml, html);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
console.log("\n " + C.green + "โ Project successfully scaffolded!" + R + "\n");
|
|
544
|
-
console.log(" " + B + "Next steps:" + R);
|
|
545
|
-
console.log(" cd " + targetDir);
|
|
546
|
-
if (isPython) {
|
|
547
|
-
console.log(" python main.py\n");
|
|
548
|
-
} else if (isNode) {
|
|
549
|
-
console.log(" node index.mjs\n");
|
|
550
|
-
} else {
|
|
551
|
-
if (isVite && pm !== 'bun') {
|
|
552
|
-
console.log(" " + pm + " install");
|
|
553
|
-
}
|
|
554
|
-
console.log(" " + devCmd + "\n");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
} catch (err) {
|
|
558
|
-
console.error("\n " + C.red + "Error during scaffolding:" + R, err.message);
|
|
559
|
-
process.exit(1);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// โโ Terminal Demo Logic โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
564
|
-
|
|
565
|
-
async function runDemo() {
|
|
566
|
-
const WS_BASE = 'wss://api.tik.tools';
|
|
567
|
-
const DEMO_KEY = 'demo_tiktokliveapi_public_2026';
|
|
568
|
-
const CHANNELS = [
|
|
569
|
-
'gbnews', 'cgtnofficial', 'france24_en',
|
|
570
|
-
'weathernewslive', 'gbnews', 'bbcnews',
|
|
571
|
-
'skynews', 'tv_asahi_news', 'abc7chicago', 'thairath_news',
|
|
572
|
-
];
|
|
573
|
-
|
|
574
|
-
const TAG = {
|
|
575
|
-
chat: C.cyan + "chat" + R,
|
|
576
|
-
gift: C.yellow + "gift" + R,
|
|
577
|
-
like: C.mag + "like" + R,
|
|
578
|
-
member: C.green + "join" + R,
|
|
579
|
-
follow: C.green + "follow" + R,
|
|
580
|
-
viewer: C.blue + "viewers" + R,
|
|
581
|
-
share: C.white + "share" + R,
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
let target = '';
|
|
585
|
-
let apiKey = DEMO_KEY;
|
|
586
|
-
for (let i = 0; i < args.length; i++) {
|
|
587
|
-
const a = args[i];
|
|
588
|
-
if (a === '--key' || a === '-k') { apiKey = args[++i] || DEMO_KEY; }
|
|
589
|
-
else if (a === '--help' || a === '-h') { help(); process.exit(0); }
|
|
590
|
-
else if (!a.startsWith('-')) { target = a.replace(/^@/, ''); }
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function help() {
|
|
594
|
-
console.log("\n " + B + "tiktok-live-api" + R + " " + D + "Real-time TikTok Live SDK" + R);
|
|
595
|
-
console.log("\n " + B + "Usage" + R);
|
|
596
|
-
console.log(" " + C.cyan + "npx tiktok-live-api" + R + " " + D + "interactive menu (demo or scaffold)" + R);
|
|
597
|
-
console.log(" " + C.cyan + "npx tiktok-live-api" + R + " " + C.white + "@username" + R + " " + D + "connect to a specific user" + R);
|
|
598
|
-
console.log(" " + C.cyan + "npx tiktok-live-api" + R + " " + C.gray + "--key KEY" + R + " " + D + "use your own API key" + R);
|
|
599
|
-
console.log("\n " + D + "Unofficial API by TikTool ยท https://tik.tools" + R + "\n");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function probe(uid, ms = 8000) {
|
|
603
|
-
return new Promise(resolve => {
|
|
604
|
-
const ws = new WebSocket(WS_BASE + "?uniqueId=" + uid + "&apiKey=" + apiKey);
|
|
605
|
-
const t = setTimeout(() => { ws.close(); resolve(null); }, ms);
|
|
606
|
-
ws.on('message', () => { clearTimeout(t); resolve(ws); });
|
|
607
|
-
ws.on('error', () => { clearTimeout(t); resolve(null); });
|
|
608
|
-
ws.on('close', () => { clearTimeout(t); });
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function ts() {
|
|
613
|
-
const d = new Date();
|
|
614
|
-
return C.gray + String(d.getHours()).padStart(2, '0') + ":" + String(d.getMinutes()).padStart(2, '0') + ":" + String(d.getSeconds()).padStart(2, '0') + R;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function fmt(event, data) {
|
|
618
|
-
const tag = TAG[event] || (C.gray + event.padEnd(7) + R);
|
|
619
|
-
const u = data.user?.uniqueId || '';
|
|
620
|
-
const pad = tag.length < 20 ? ' ' : ' ';
|
|
621
|
-
|
|
622
|
-
switch (event) {
|
|
623
|
-
case 'chat': return ts() + " " + tag + pad + " " + B + u + R + " " + (data.comment || '');
|
|
624
|
-
case 'gift': return ts() + " " + tag + pad + " " + B + u + R + " " + C.yellow + (data.giftName || 'gift') + R + " x" + (data.repeatCount || 1) + " " + D + "(" + (data.diamondCount || 0) + "๐)" + R;
|
|
625
|
-
case 'like': return ts() + " " + tag + pad + " " + B + u + R + " " + D + "total " + (data.totalLikeCount || 0).toLocaleString() + R;
|
|
626
|
-
case 'member': return ts() + " " + tag + pad + " " + C.green + u + R;
|
|
627
|
-
case 'follow': return ts() + " " + tag + " " + C.green + u + R;
|
|
628
|
-
case 'roomUserSeq':
|
|
629
|
-
case 'viewer': return ts() + " " + TAG.viewer + " " + B + (data.viewerCount || 0).toLocaleString() + R;
|
|
630
|
-
case 'share': return ts() + " " + tag + " " + u;
|
|
631
|
-
default: return null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
console.log("\n " + B + "tiktok-live-api" + R + " " + D + "ยท" + R + " " + D + "Real-time TikTok Live events" + R);
|
|
636
|
-
console.log(" " + D + "Unofficial API by TikTool ยท https://tik.tools" + R + "\n");
|
|
637
|
-
|
|
638
|
-
let ws = null, who = '';
|
|
639
|
-
|
|
640
|
-
if (target) {
|
|
641
|
-
process.stdout.write(" " + D + "connecting to" + R + " " + B + "@" + target + R + " " + D + "..." + R);
|
|
642
|
-
ws = await probe(target);
|
|
643
|
-
if (ws) { who = target; console.log(" " + C.green + "โ" + R); }
|
|
644
|
-
else { console.log(" " + C.red + "โ" + R + "\n\n " + C.red + "Stream not found." + R + " Make sure " + B + "@" + target + R + " is live.\n"); process.exit(1); }
|
|
645
|
-
} else {
|
|
646
|
-
console.log(" " + D + "scanning for live streams..." + R + "\n");
|
|
647
|
-
for (const uid of CHANNELS) {
|
|
648
|
-
process.stdout.write(" " + C.gray + "@" + uid + R);
|
|
649
|
-
ws = await probe(uid);
|
|
650
|
-
if (ws) { who = uid; console.log(" " + C.green + "โ live" + R); break; }
|
|
651
|
-
else { console.log(" " + D + "โ" + R); }
|
|
652
|
-
}
|
|
653
|
-
if (!ws) {
|
|
654
|
-
console.log("\n " + C.red + "No live streams found." + R + " Try: " + C.cyan + "npx tiktok-live-api @username" + R + "\n");
|
|
655
|
-
console.log(" " + D + "Get your own API key at" + R + " " + C.cyan + "https://tik.tools" + R + "\n");
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
console.log("\n " + C.green + "โ" + R + " " + B + "@" + who + R + " " + D + "โ streaming events. Ctrl+C to stop." + R);
|
|
661
|
-
console.log(" " + D + "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ" + R + "\n");
|
|
662
|
-
|
|
663
|
-
let count = 0;
|
|
664
|
-
ws.on('message', raw => {
|
|
665
|
-
try {
|
|
666
|
-
const m = JSON.parse(raw.toString());
|
|
667
|
-
const line = fmt(m.event || 'unknown', m.data || m);
|
|
668
|
-
if (line) { count++; console.log(" " + line); }
|
|
669
|
-
} catch { }
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
ws.on('close', () => {
|
|
673
|
-
console.log("\n " + D + "disconnected โ " + B + count + R + D + " events received" + R);
|
|
674
|
-
console.log(" " + D + "Get unlimited access:" + R + " " + C.cyan + "https://tik.tools" + R + "\n");
|
|
675
|
-
process.exit(0);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
ws.on('error', e => console.error(" " + C.red + "error" + R + " " + e.message));
|
|
679
|
-
process.on('SIGINT', () => {
|
|
680
|
-
console.log("\n\n " + D + "stopped โ " + B + count + R + D + " events received" + R);
|
|
681
|
-
console.log(" " + D + "Get unlimited access:" + R + " " + C.cyan + "https://tik.tools" + R + "\n");
|
|
682
|
-
ws.close();
|
|
683
|
-
process.exit(0);
|
|
684
|
-
});
|
|
685
|
-
}
|