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 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)** &nbsp;|&nbsp; ๐Ÿ **Python SDK โ†’ [tik.tools/guides/python-tiktok-live](https://tik.tools/guides/python-tiktok-live)** &nbsp;|&nbsp; ๐Ÿ”Œ **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
  [![npm](https://img.shields.io/npm/v/tiktok-live-api)](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` | Battle event | `type`, `teams`, `scores` |
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
- ## Live Captions (Speech-to-Text)
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 { TikTokCaptions } from 'tiktok-live-api';
210
+ import { TikTokLive } from 'tiktok-live-api';
220
211
 
221
- <<<<<<< HEAD
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
- ### Reference
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
- ## ๐ŸŽค Real-Time Live Captions
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
- AI-powered speech-to-text transcription and translation for TikTok LIVE streams. Features include:
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
- ### Quick Start
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 '@tiktool/live';
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) | $7/week |
428
- | Pro | 75,000 | 50 (8h) | $15/week |
429
- | Ultra | 300,000 | 500 (8h) | $45/week |
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.2.18",
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
- }