untiktok-api 1.0.0
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 -0
- package/LICENSE.txt +21 -0
- package/README.md +80 -0
- package/dist/api/comment.d.ts +42 -0
- package/dist/api/comment.js +84 -0
- package/dist/api/hashtag.d.ts +52 -0
- package/dist/api/hashtag.js +118 -0
- package/dist/api/playlist.d.ts +53 -0
- package/dist/api/playlist.js +112 -0
- package/dist/api/search.d.ts +38 -0
- package/dist/api/search.js +98 -0
- package/dist/api/sound.d.ts +57 -0
- package/dist/api/sound.js +133 -0
- package/dist/api/trending.d.ts +21 -0
- package/dist/api/trending.js +52 -0
- package/dist/api/user.d.ts +187 -0
- package/dist/api/user.js +498 -0
- package/dist/api/video.d.ts +119 -0
- package/dist/api/video.js +399 -0
- package/dist/exceptions.d.ts +24 -0
- package/dist/exceptions.js +61 -0
- package/dist/helpers.d.ts +23 -0
- package/dist/helpers.js +66 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +35 -0
- package/dist/stealth/index.d.ts +70 -0
- package/dist/stealth/index.js +128 -0
- package/dist/stealth/js/chrome_app.d.ts +1 -0
- package/dist/stealth/js/chrome_app.js +27 -0
- package/dist/stealth/js/chrome_csi.d.ts +1 -0
- package/dist/stealth/js/chrome_csi.js +14 -0
- package/dist/stealth/js/chrome_hairline.d.ts +1 -0
- package/dist/stealth/js/chrome_hairline.js +16 -0
- package/dist/stealth/js/chrome_load_times.d.ts +1 -0
- package/dist/stealth/js/chrome_load_times.js +28 -0
- package/dist/stealth/js/chrome_runtime_script.d.ts +1 -0
- package/dist/stealth/js/chrome_runtime_script.js +84 -0
- package/dist/stealth/js/generate_magic_arrays.d.ts +1 -0
- package/dist/stealth/js/generate_magic_arrays.js +28 -0
- package/dist/stealth/js/iframe_contentWindow.d.ts +1 -0
- package/dist/stealth/js/iframe_contentWindow.js +22 -0
- package/dist/stealth/js/media_codecs.d.ts +1 -0
- package/dist/stealth/js/media_codecs.js +16 -0
- package/dist/stealth/js/navigator_hardwareConcurrency.d.ts +1 -0
- package/dist/stealth/js/navigator_hardwareConcurrency.js +6 -0
- package/dist/stealth/js/navigator_languages.d.ts +1 -0
- package/dist/stealth/js/navigator_languages.js +6 -0
- package/dist/stealth/js/navigator_permissions.d.ts +1 -0
- package/dist/stealth/js/navigator_permissions.js +11 -0
- package/dist/stealth/js/navigator_platform.d.ts +1 -0
- package/dist/stealth/js/navigator_platform.js +8 -0
- package/dist/stealth/js/navigator_plugins_script.d.ts +1 -0
- package/dist/stealth/js/navigator_plugins_script.js +37 -0
- package/dist/stealth/js/navigator_userAgent_script.d.ts +1 -0
- package/dist/stealth/js/navigator_userAgent_script.js +8 -0
- package/dist/stealth/js/navigator_vendor_script.d.ts +1 -0
- package/dist/stealth/js/navigator_vendor_script.js +6 -0
- package/dist/stealth/js/utils_script.d.ts +1 -0
- package/dist/stealth/js/utils_script.js +119 -0
- package/dist/stealth/js/webgl_vendor_script.d.ts +1 -0
- package/dist/stealth/js/webgl_vendor_script.js +16 -0
- package/dist/stealth/js/window_outerdimensions.d.ts +1 -0
- package/dist/stealth/js/window_outerdimensions.js +9 -0
- package/dist/tiktok.d.ts +96 -0
- package/dist/tiktok.js +758 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +6 -0
- package/package.json +41 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// api/video.ts
|
|
4
|
+
// Mirrors TikTokApi/api/video.py — corrected vs Python source
|
|
5
|
+
// ============================================================
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.Video = void 0;
|
|
11
|
+
const axios_1 = __importDefault(require("axios"));
|
|
12
|
+
const exceptions_1 = require("../exceptions");
|
|
13
|
+
class Video {
|
|
14
|
+
/** Gets the description/caption of the video */
|
|
15
|
+
get description() {
|
|
16
|
+
return this.asDict?.["desc"] ?? null;
|
|
17
|
+
}
|
|
18
|
+
/** Gets the play/view count of the video */
|
|
19
|
+
get plays() {
|
|
20
|
+
return this.stats?.["playCount"] || 0;
|
|
21
|
+
}
|
|
22
|
+
/** Gets the digg/like count of the video */
|
|
23
|
+
get likes() {
|
|
24
|
+
return this.stats?.["diggCount"] || 0;
|
|
25
|
+
}
|
|
26
|
+
/** Gets the comment count of the video */
|
|
27
|
+
get commentsCount() {
|
|
28
|
+
return this.stats?.["commentCount"] || 0;
|
|
29
|
+
}
|
|
30
|
+
/** Gets the share count of the video */
|
|
31
|
+
get shares() {
|
|
32
|
+
return this.stats?.["shareCount"] || 0;
|
|
33
|
+
}
|
|
34
|
+
/** Gets the collect/save count of the video */
|
|
35
|
+
get saves() {
|
|
36
|
+
return this.stats?.["collectCount"] || 0;
|
|
37
|
+
}
|
|
38
|
+
/** Gets whether the video is pinned by the creator */
|
|
39
|
+
get isPinned() {
|
|
40
|
+
const itemControl = this.asDict?.["itemControl"];
|
|
41
|
+
return Boolean(this.asDict?.["isPinnedItem"]) || Boolean(itemControl?.["isPinned"]);
|
|
42
|
+
}
|
|
43
|
+
constructor(parent, { id, url, data, sessionIndex, proxy } = {}) {
|
|
44
|
+
this.parent = parent;
|
|
45
|
+
this.id = id ?? undefined;
|
|
46
|
+
this.url = url ?? undefined;
|
|
47
|
+
if (data) {
|
|
48
|
+
this.asDict = data;
|
|
49
|
+
this._extractFromData();
|
|
50
|
+
}
|
|
51
|
+
else if (url) {
|
|
52
|
+
// Python calls extract_video_id_from_url synchronously in __init__ using _get_session.
|
|
53
|
+
// In TypeScript the session is synchronous too — we replicate that via the sync _getSession.
|
|
54
|
+
const [, session] = this.parent._getSession({ sessionIndex });
|
|
55
|
+
const proxyVal = proxy ?? session.proxy;
|
|
56
|
+
// Resolve synchronously: run a sync HEAD follow-redirect.
|
|
57
|
+
// We extract the id by parsing the URL if it already contains /video/;
|
|
58
|
+
// otherwise we schedule an async resolution and store the raw url.
|
|
59
|
+
// If the url already has the video id embedded we resolve it immediately.
|
|
60
|
+
if (url.includes("/video/")) {
|
|
61
|
+
this.id = url.split("/video/")[1].split("?")[0];
|
|
62
|
+
}
|
|
63
|
+
// If not, the caller must await Video.fromUrl() instead.
|
|
64
|
+
}
|
|
65
|
+
if (!this.id && !this.url) {
|
|
66
|
+
if (data) {
|
|
67
|
+
this.parent.logger.warn(`Video data provided is missing 'id': ${JSON.stringify(data)}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new TypeError("You must provide id or url parameter.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Async factory that follows redirects to resolve the video ID from a short URL.
|
|
76
|
+
* Use this instead of `new Video({ url })` when you have a short/redirect URL.
|
|
77
|
+
*/
|
|
78
|
+
static async fromUrl(parent, url, kwargs = {}) {
|
|
79
|
+
const [, session] = parent._getSession(kwargs);
|
|
80
|
+
const proxyVal = kwargs.proxy ?? session.proxy;
|
|
81
|
+
const response = await axios_1.default.head(url, {
|
|
82
|
+
headers: session.headers ?? {},
|
|
83
|
+
maxRedirects: 10,
|
|
84
|
+
});
|
|
85
|
+
const finalUrl = response.request?.res?.responseUrl ?? url;
|
|
86
|
+
if (finalUrl.includes("@") && finalUrl.includes("/video/")) {
|
|
87
|
+
const videoId = finalUrl.split("/video/")[1].split("?")[0];
|
|
88
|
+
return new Video(parent, { id: videoId, url });
|
|
89
|
+
}
|
|
90
|
+
throw new TypeError("URL format not supported. Example:\nhttps://www.tiktok.com/@therock/video/6829267836783971589");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns a dictionary of all data associated with a TikTok Video.
|
|
94
|
+
* Note: This is slow since it requires an HTTP request.
|
|
95
|
+
*
|
|
96
|
+
* Python uses `requests.get`; TS uses `axios.get` (equivalent sync-style).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const info = await api.video({ url: 'https://www.tiktok.com/@.../video/...' }).info();
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
async info(kwargs = {}) {
|
|
104
|
+
const [, session] = this.parent._getSession(kwargs);
|
|
105
|
+
const proxy = kwargs.proxy ?? session.proxy;
|
|
106
|
+
if (!this.url) {
|
|
107
|
+
throw new TypeError("To call video.info() you need to set the video's url.");
|
|
108
|
+
}
|
|
109
|
+
let text;
|
|
110
|
+
let statusCode = 200;
|
|
111
|
+
let setCookieHeader = undefined;
|
|
112
|
+
try {
|
|
113
|
+
await session.page.goto(this.url, { waitUntil: "domcontentloaded" });
|
|
114
|
+
let found = false;
|
|
115
|
+
const maxAttempts = 15;
|
|
116
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
117
|
+
const hasTag = await session.page.evaluate(() => {
|
|
118
|
+
const doc = globalThis.document;
|
|
119
|
+
return !!(doc?.getElementById("__UNIVERSAL_DATA_FOR_REHYDRATION__") ||
|
|
120
|
+
doc?.getElementById("SIGI_STATE"));
|
|
121
|
+
});
|
|
122
|
+
if (hasTag) {
|
|
123
|
+
found = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
127
|
+
}
|
|
128
|
+
if (!found) {
|
|
129
|
+
throw new Error("Script tags (__UNIVERSAL_DATA_FOR_REHYDRATION__ or SIGI_STATE) not found in page DOM after timeout.");
|
|
130
|
+
}
|
|
131
|
+
text = await session.page.content();
|
|
132
|
+
}
|
|
133
|
+
catch (fetchErr) {
|
|
134
|
+
this.parent.logger.warn(`Browser navigation failed for video.info(), falling back to axios: ${fetchErr}`);
|
|
135
|
+
const response = await axios_1.default.get(this.url, {
|
|
136
|
+
headers: session.headers ?? {},
|
|
137
|
+
responseType: "text",
|
|
138
|
+
});
|
|
139
|
+
statusCode = response.status;
|
|
140
|
+
if (response.status !== 200) {
|
|
141
|
+
throw new exceptions_1.InvalidResponseException(response.data, "TikTok returned an invalid response.", response.status);
|
|
142
|
+
}
|
|
143
|
+
text = response.data;
|
|
144
|
+
setCookieHeader = response.headers["set-cookie"];
|
|
145
|
+
}
|
|
146
|
+
let videoInfo;
|
|
147
|
+
// Try SIGI_STATE first (same logic as Python)
|
|
148
|
+
const sigiTag = '<script id="SIGI_STATE" type="application/json">';
|
|
149
|
+
const sigiStart = text.indexOf(sigiTag);
|
|
150
|
+
if (sigiStart !== -1) {
|
|
151
|
+
const contentStart = sigiStart + sigiTag.length;
|
|
152
|
+
const contentEnd = text.indexOf("</script>", contentStart);
|
|
153
|
+
if (contentEnd === -1) {
|
|
154
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response.", statusCode);
|
|
155
|
+
}
|
|
156
|
+
const data = JSON.parse(text.slice(contentStart, contentEnd));
|
|
157
|
+
const itemModule = data["ItemModule"];
|
|
158
|
+
if (!itemModule) {
|
|
159
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response. 'ItemModule' not found in SIGI_STATE.", statusCode);
|
|
160
|
+
}
|
|
161
|
+
videoInfo = itemModule[this.id];
|
|
162
|
+
if (!videoInfo) {
|
|
163
|
+
throw new exceptions_1.InvalidResponseException(text, `TikTok returned an invalid response. Video ID ${this.id} not found in ItemModule.`, statusCode);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Try __UNIVERSAL_DATA_FOR_REHYDRATION__
|
|
168
|
+
const rehydTag = '<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">';
|
|
169
|
+
const rehydStart = text.indexOf(rehydTag);
|
|
170
|
+
if (rehydStart === -1) {
|
|
171
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response.", statusCode);
|
|
172
|
+
}
|
|
173
|
+
const contentStart = rehydStart + rehydTag.length;
|
|
174
|
+
const contentEnd = text.indexOf("</script>", contentStart);
|
|
175
|
+
if (contentEnd === -1) {
|
|
176
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response.", statusCode);
|
|
177
|
+
}
|
|
178
|
+
const data = JSON.parse(text.slice(contentStart, contentEnd));
|
|
179
|
+
const defaultScope = (data["__DEFAULT_SCOPE__"] ?? {});
|
|
180
|
+
const videoDetail = (defaultScope["webapp.video-detail"] ?? {});
|
|
181
|
+
if ((videoDetail["statusCode"] ?? 0) !== 0) {
|
|
182
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response structure.", statusCode);
|
|
183
|
+
}
|
|
184
|
+
videoInfo = (videoDetail["itemInfo"]?.["itemStruct"]);
|
|
185
|
+
if (!videoInfo) {
|
|
186
|
+
throw new exceptions_1.InvalidResponseException(text, "TikTok returned an invalid response structure.", statusCode);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
this.asDict = videoInfo;
|
|
190
|
+
this._extractFromData();
|
|
191
|
+
// Convert Set-Cookie headers to Playwright cookie format and store them
|
|
192
|
+
// (mirrors Python's `requests_cookie_to_playwright_cookie`)
|
|
193
|
+
if (setCookieHeader) {
|
|
194
|
+
const cookies = _parseCookieHeaders(setCookieHeader, this.url);
|
|
195
|
+
await this.parent.setSessionCookies(session, cookies);
|
|
196
|
+
}
|
|
197
|
+
return videoInfo;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the raw bytes of a TikTok Video.
|
|
201
|
+
*
|
|
202
|
+
* Python uses `requests.get` / `httpx.AsyncClient` for streaming.
|
|
203
|
+
* TS uses `axios` equivalents.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* const buf = await video.bytes() as Buffer;
|
|
208
|
+
* fs.writeFileSync('video.mp4', buf);
|
|
209
|
+
*
|
|
210
|
+
* // Streaming
|
|
211
|
+
* for await (const chunk of await video.bytes({ stream: true }) as AsyncGenerator<Buffer>) { ... }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
async bytes(options = {}) {
|
|
215
|
+
// Python: i, session = self.parent._get_session(**kwargs)
|
|
216
|
+
const [, session] = this.parent._getSession(options);
|
|
217
|
+
const videoData = (this.asDict?.["video"] ?? {});
|
|
218
|
+
const downloadAddr = videoData["downloadAddr"] || videoData["playAddr"];
|
|
219
|
+
if (!downloadAddr) {
|
|
220
|
+
throw new Error("No download address found. Have you called .info() first?");
|
|
221
|
+
}
|
|
222
|
+
// Python sets cookies as a dict directly from get_session_cookies
|
|
223
|
+
const cookies = await this.parent.getSessionCookies(session);
|
|
224
|
+
const cookieString = Object.entries(cookies)
|
|
225
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
226
|
+
.join("; ");
|
|
227
|
+
// Python mutates session.headers — we create a shallow copy here
|
|
228
|
+
const headers = {
|
|
229
|
+
...(session.headers ?? {}),
|
|
230
|
+
range: "bytes=0-",
|
|
231
|
+
"accept-encoding": "identity;q=1, *;q=0",
|
|
232
|
+
referer: "https://www.tiktok.com/",
|
|
233
|
+
cookie: cookieString,
|
|
234
|
+
};
|
|
235
|
+
if (options.stream) {
|
|
236
|
+
async function* streamGen() {
|
|
237
|
+
const resp = await axios_1.default.get(downloadAddr, {
|
|
238
|
+
headers,
|
|
239
|
+
responseType: "stream",
|
|
240
|
+
});
|
|
241
|
+
for await (const chunk of resp.data) {
|
|
242
|
+
// cast via unknown to avoid TS string↔Uint8Array overlap error
|
|
243
|
+
yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return streamGen();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Python: resp = requests.get(downloadAddr, headers=h, cookies=cookies)
|
|
250
|
+
// return resp.content
|
|
251
|
+
const resp = await axios_1.default.get(downloadAddr, {
|
|
252
|
+
headers,
|
|
253
|
+
responseType: "arraybuffer",
|
|
254
|
+
});
|
|
255
|
+
return Buffer.from(resp.data);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Returns the comments of a TikTok Video.
|
|
260
|
+
*
|
|
261
|
+
* Python key: `has_more` (snake_case) — preserved in TS.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* for await (const comment of video.comments(20)) { ... }
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
async *comments(count = 20, cursor = 0, kwargs = {}) {
|
|
269
|
+
let found = 0;
|
|
270
|
+
while (found < count) {
|
|
271
|
+
const params = {
|
|
272
|
+
aweme_id: this.id,
|
|
273
|
+
count: 20,
|
|
274
|
+
cursor,
|
|
275
|
+
};
|
|
276
|
+
const resp = await this.parent.makeRequest({
|
|
277
|
+
url: "https://www.tiktok.com/api/comment/list/",
|
|
278
|
+
params,
|
|
279
|
+
headers: kwargs.headers,
|
|
280
|
+
sessionIndex: kwargs.sessionIndex,
|
|
281
|
+
});
|
|
282
|
+
if (resp == null) {
|
|
283
|
+
throw new exceptions_1.InvalidResponseException(resp, "TikTok returned an invalid response.");
|
|
284
|
+
}
|
|
285
|
+
const comments = resp["comments"] ?? [];
|
|
286
|
+
for (const comment of comments) {
|
|
287
|
+
yield this.parent.comment({ data: comment });
|
|
288
|
+
found++;
|
|
289
|
+
}
|
|
290
|
+
// Python: if not resp.get("has_more", False): return
|
|
291
|
+
if (!resp["has_more"])
|
|
292
|
+
return;
|
|
293
|
+
cursor = resp["cursor"];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns related videos of a TikTok Video.
|
|
298
|
+
* Note: Python's related_videos does NOT increment `found` after the inner loop — bug or intentional.
|
|
299
|
+
* We preserve that exact behaviour (no double-increment).
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* for await (const rel of video.relatedVideos(30)) { ... }
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
async *relatedVideos(count = 30, cursor = 0, kwargs = {}) {
|
|
307
|
+
let found = 0;
|
|
308
|
+
while (found < count) {
|
|
309
|
+
const params = {
|
|
310
|
+
itemID: this.id,
|
|
311
|
+
count: 16,
|
|
312
|
+
};
|
|
313
|
+
const resp = await this.parent.makeRequest({
|
|
314
|
+
url: "https://www.tiktok.com/api/related/item_list/",
|
|
315
|
+
params,
|
|
316
|
+
headers: kwargs.headers,
|
|
317
|
+
sessionIndex: kwargs.sessionIndex,
|
|
318
|
+
});
|
|
319
|
+
if (resp == null) {
|
|
320
|
+
throw new exceptions_1.InvalidResponseException(resp, "TikTok returned an invalid response.");
|
|
321
|
+
}
|
|
322
|
+
const itemList = resp["itemList"] ?? [];
|
|
323
|
+
for (const item of itemList) {
|
|
324
|
+
yield this.parent.video({ data: item });
|
|
325
|
+
found++;
|
|
326
|
+
}
|
|
327
|
+
// Note: Python has no hasMore check here — it just loops once.
|
|
328
|
+
// We match that: break after first page like Python.
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
_extractFromData() {
|
|
333
|
+
const data = this.asDict;
|
|
334
|
+
this.id = data["id"];
|
|
335
|
+
const timestamp = data["createTime"];
|
|
336
|
+
if (timestamp != null) {
|
|
337
|
+
try {
|
|
338
|
+
const ts = typeof timestamp === "string" ? parseInt(timestamp, 10) : timestamp;
|
|
339
|
+
this.createTime = new Date(ts * 1000);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// ignore parse error
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Python: self.stats = data.get("statsV2") or data.get("stats")
|
|
346
|
+
this.stats = (data["statsV2"] ?? data["stats"]);
|
|
347
|
+
// Python: author = data.get("author"); if isinstance(author, str): ...
|
|
348
|
+
const author = data["author"];
|
|
349
|
+
if (typeof author === "string") {
|
|
350
|
+
this.author = this.parent.user({ username: author });
|
|
351
|
+
}
|
|
352
|
+
else if (author) {
|
|
353
|
+
this.author = this.parent.user({ data: author });
|
|
354
|
+
}
|
|
355
|
+
this.sound = this.parent.sound({ data: data });
|
|
356
|
+
const challenges = data["challenges"] ?? [];
|
|
357
|
+
this.hashtags = challenges.map((h) => this.parent.hashtag({ data: h }));
|
|
358
|
+
if (!this.id) {
|
|
359
|
+
this.parent.logger.error(`Failed to create Video with data: ${JSON.stringify(data)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
toString() {
|
|
363
|
+
return `TikTokApi.video(id='${this.id}')`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
exports.Video = Video;
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Helper: parse Set-Cookie headers into Playwright cookie format
|
|
369
|
+
// (mirrors Python's requests_cookie_to_playwright_cookie)
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
function _parseCookieHeaders(setCookieHeader, sourceUrl) {
|
|
372
|
+
const headers = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
373
|
+
let domain = "";
|
|
374
|
+
try {
|
|
375
|
+
domain = new URL(sourceUrl).hostname;
|
|
376
|
+
}
|
|
377
|
+
catch { /* ignore */ }
|
|
378
|
+
return headers.map((h) => {
|
|
379
|
+
const parts = h.split(";").map((p) => p.trim());
|
|
380
|
+
const [nameVal, ...attrs] = parts;
|
|
381
|
+
const eqIdx = nameVal.indexOf("=");
|
|
382
|
+
const name = nameVal.slice(0, eqIdx);
|
|
383
|
+
const value = nameVal.slice(eqIdx + 1);
|
|
384
|
+
const cookie = { name, value, domain, path: "/" };
|
|
385
|
+
for (const attr of attrs) {
|
|
386
|
+
const lower = attr.toLowerCase();
|
|
387
|
+
if (lower === "secure")
|
|
388
|
+
cookie["secure"] = true;
|
|
389
|
+
if (lower.startsWith("path="))
|
|
390
|
+
cookie["path"] = attr.slice(5);
|
|
391
|
+
if (lower.startsWith("expires=")) {
|
|
392
|
+
const exp = new Date(attr.slice(8)).getTime() / 1000;
|
|
393
|
+
if (!isNaN(exp))
|
|
394
|
+
cookie["expires"] = exp;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return cookie;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class TikTokException extends Error {
|
|
2
|
+
errorCode: number | undefined;
|
|
3
|
+
rawResponse: unknown;
|
|
4
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
5
|
+
toString(): string;
|
|
6
|
+
}
|
|
7
|
+
export declare class CaptchaException extends TikTokException {
|
|
8
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
9
|
+
}
|
|
10
|
+
export declare class NotFoundException extends TikTokException {
|
|
11
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
12
|
+
}
|
|
13
|
+
export declare class EmptyResponseException extends TikTokException {
|
|
14
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
15
|
+
}
|
|
16
|
+
export declare class SoundRemovedException extends TikTokException {
|
|
17
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
18
|
+
}
|
|
19
|
+
export declare class InvalidJSONException extends TikTokException {
|
|
20
|
+
constructor(rawResponse?: unknown, message?: string, errorCode?: number);
|
|
21
|
+
}
|
|
22
|
+
export declare class InvalidResponseException extends TikTokException {
|
|
23
|
+
constructor(rawResponse: unknown, message: string, errorCode?: number);
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// exceptions.ts
|
|
4
|
+
// Mirrors TikTokApi/exceptions.py
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.InvalidResponseException = exports.InvalidJSONException = exports.SoundRemovedException = exports.EmptyResponseException = exports.NotFoundException = exports.CaptchaException = exports.TikTokException = void 0;
|
|
8
|
+
class TikTokException extends Error {
|
|
9
|
+
constructor(rawResponse, message, errorCode) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "TikTokException";
|
|
12
|
+
this.rawResponse = rawResponse;
|
|
13
|
+
this.errorCode = errorCode;
|
|
14
|
+
}
|
|
15
|
+
toString() {
|
|
16
|
+
return `${this.errorCode} -> ${this.message}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.TikTokException = TikTokException;
|
|
20
|
+
class CaptchaException extends TikTokException {
|
|
21
|
+
constructor(rawResponse, message, errorCode) {
|
|
22
|
+
super(rawResponse, message, errorCode);
|
|
23
|
+
this.name = "CaptchaException";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.CaptchaException = CaptchaException;
|
|
27
|
+
class NotFoundException extends TikTokException {
|
|
28
|
+
constructor(rawResponse, message, errorCode) {
|
|
29
|
+
super(rawResponse, message, errorCode);
|
|
30
|
+
this.name = "NotFoundException";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.NotFoundException = NotFoundException;
|
|
34
|
+
class EmptyResponseException extends TikTokException {
|
|
35
|
+
constructor(rawResponse, message, errorCode) {
|
|
36
|
+
super(rawResponse, message, errorCode);
|
|
37
|
+
this.name = "EmptyResponseException";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.EmptyResponseException = EmptyResponseException;
|
|
41
|
+
class SoundRemovedException extends TikTokException {
|
|
42
|
+
constructor(rawResponse, message, errorCode) {
|
|
43
|
+
super(rawResponse, message, errorCode);
|
|
44
|
+
this.name = "SoundRemovedException";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.SoundRemovedException = SoundRemovedException;
|
|
48
|
+
class InvalidJSONException extends TikTokException {
|
|
49
|
+
constructor(rawResponse, message, errorCode) {
|
|
50
|
+
super(rawResponse, message ?? "TikTok returned invalid JSON", errorCode);
|
|
51
|
+
this.name = "InvalidJSONException";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.InvalidJSONException = InvalidJSONException;
|
|
55
|
+
class InvalidResponseException extends TikTokException {
|
|
56
|
+
constructor(rawResponse, message, errorCode) {
|
|
57
|
+
super(rawResponse, message, errorCode);
|
|
58
|
+
this.name = "InvalidResponseException";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.InvalidResponseException = InvalidResponseException;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the video ID from a TikTok URL, following redirects.
|
|
3
|
+
*/
|
|
4
|
+
export declare function extractVideoIdFromUrl(url: string, headers?: Record<string, string>, proxy?: string | null): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Return a random element from an array, or undefined if empty/null.
|
|
7
|
+
*/
|
|
8
|
+
export declare function randomChoice<T>(choices: T[] | null | undefined): T | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Convert an axios/http cookie object to the Playwright cookie format.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cookieToPlaywrightCookie(cookie: {
|
|
13
|
+
name: string;
|
|
14
|
+
value: string;
|
|
15
|
+
domain?: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
secure?: boolean;
|
|
18
|
+
expires?: number;
|
|
19
|
+
}): Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Sleep for `ms` milliseconds.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sleep(ms: number): Promise<void>;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// helpers.ts
|
|
4
|
+
// Mirrors TikTokApi/helpers.py
|
|
5
|
+
// ============================================================
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.extractVideoIdFromUrl = extractVideoIdFromUrl;
|
|
11
|
+
exports.randomChoice = randomChoice;
|
|
12
|
+
exports.cookieToPlaywrightCookie = cookieToPlaywrightCookie;
|
|
13
|
+
exports.sleep = sleep;
|
|
14
|
+
const axios_1 = __importDefault(require("axios"));
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
/**
|
|
17
|
+
* Extract the video ID from a TikTok URL, following redirects.
|
|
18
|
+
*/
|
|
19
|
+
async function extractVideoIdFromUrl(url, headers = {}, proxy) {
|
|
20
|
+
const response = await axios_1.default.head(url, {
|
|
21
|
+
headers,
|
|
22
|
+
maxRedirects: 10,
|
|
23
|
+
// axios does not accept a raw proxy string the same way requests does —
|
|
24
|
+
// the caller should configure an httpsAgent if a proxy is needed.
|
|
25
|
+
});
|
|
26
|
+
// axios stores the final URL in response.request?.res?.responseUrl
|
|
27
|
+
// or we can use response.request.path on some versions.
|
|
28
|
+
// The safest approach is to use the `responseUrl` from the underlying http request.
|
|
29
|
+
const finalUrl = response.request?.res?.responseUrl ??
|
|
30
|
+
url;
|
|
31
|
+
if (finalUrl.includes("@") && finalUrl.includes("/video/")) {
|
|
32
|
+
return finalUrl.split("/video/")[1].split("?")[0];
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError("URL format not supported. Example of a supported URL:\n" +
|
|
35
|
+
"https://www.tiktok.com/@therock/video/6829267836783971589");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Return a random element from an array, or undefined if empty/null.
|
|
39
|
+
*/
|
|
40
|
+
function randomChoice(choices) {
|
|
41
|
+
if (!choices || choices.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return choices[(0, crypto_1.randomInt)(choices.length)];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Convert an axios/http cookie object to the Playwright cookie format.
|
|
47
|
+
*/
|
|
48
|
+
function cookieToPlaywrightCookie(cookie) {
|
|
49
|
+
const c = {
|
|
50
|
+
name: cookie.name,
|
|
51
|
+
value: cookie.value,
|
|
52
|
+
domain: cookie.domain ?? "",
|
|
53
|
+
path: cookie.path ?? "/",
|
|
54
|
+
secure: cookie.secure ?? false,
|
|
55
|
+
};
|
|
56
|
+
if (cookie.expires) {
|
|
57
|
+
c["expires"] = cookie.expires;
|
|
58
|
+
}
|
|
59
|
+
return c;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sleep for `ms` milliseconds.
|
|
63
|
+
*/
|
|
64
|
+
function sleep(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { TikTokApi } from "./tiktok";
|
|
2
|
+
export { User } from "./api/user";
|
|
3
|
+
export { Video } from "./api/video";
|
|
4
|
+
export { Sound } from "./api/sound";
|
|
5
|
+
export { Hashtag } from "./api/hashtag";
|
|
6
|
+
export { Comment } from "./api/comment";
|
|
7
|
+
export { Trending } from "./api/trending";
|
|
8
|
+
export { Search } from "./api/search";
|
|
9
|
+
export { Playlist } from "./api/playlist";
|
|
10
|
+
export { TikTokException, CaptchaException, NotFoundException, EmptyResponseException, SoundRemovedException, InvalidJSONException, InvalidResponseException, } from "./exceptions";
|
|
11
|
+
export type { TikTokPlaywrightSession, CreateSessionsOptions, ProxySettings, ResourceStats, HealthCheckResult, } from "./types";
|
|
12
|
+
export { stealthAsync, StealthConfig } from "./stealth";
|
|
13
|
+
export type { StealthConfigOptions } from "./stealth";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// index.ts — Public API surface
|
|
4
|
+
// ============================================================
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StealthConfig = exports.stealthAsync = exports.InvalidResponseException = exports.InvalidJSONException = exports.SoundRemovedException = exports.EmptyResponseException = exports.NotFoundException = exports.CaptchaException = exports.TikTokException = exports.Playlist = exports.Search = exports.Trending = exports.Comment = exports.Hashtag = exports.Sound = exports.Video = exports.User = exports.TikTokApi = void 0;
|
|
7
|
+
var tiktok_1 = require("./tiktok");
|
|
8
|
+
Object.defineProperty(exports, "TikTokApi", { enumerable: true, get: function () { return tiktok_1.TikTokApi; } });
|
|
9
|
+
var user_1 = require("./api/user");
|
|
10
|
+
Object.defineProperty(exports, "User", { enumerable: true, get: function () { return user_1.User; } });
|
|
11
|
+
var video_1 = require("./api/video");
|
|
12
|
+
Object.defineProperty(exports, "Video", { enumerable: true, get: function () { return video_1.Video; } });
|
|
13
|
+
var sound_1 = require("./api/sound");
|
|
14
|
+
Object.defineProperty(exports, "Sound", { enumerable: true, get: function () { return sound_1.Sound; } });
|
|
15
|
+
var hashtag_1 = require("./api/hashtag");
|
|
16
|
+
Object.defineProperty(exports, "Hashtag", { enumerable: true, get: function () { return hashtag_1.Hashtag; } });
|
|
17
|
+
var comment_1 = require("./api/comment");
|
|
18
|
+
Object.defineProperty(exports, "Comment", { enumerable: true, get: function () { return comment_1.Comment; } });
|
|
19
|
+
var trending_1 = require("./api/trending");
|
|
20
|
+
Object.defineProperty(exports, "Trending", { enumerable: true, get: function () { return trending_1.Trending; } });
|
|
21
|
+
var search_1 = require("./api/search");
|
|
22
|
+
Object.defineProperty(exports, "Search", { enumerable: true, get: function () { return search_1.Search; } });
|
|
23
|
+
var playlist_1 = require("./api/playlist");
|
|
24
|
+
Object.defineProperty(exports, "Playlist", { enumerable: true, get: function () { return playlist_1.Playlist; } });
|
|
25
|
+
var exceptions_1 = require("./exceptions");
|
|
26
|
+
Object.defineProperty(exports, "TikTokException", { enumerable: true, get: function () { return exceptions_1.TikTokException; } });
|
|
27
|
+
Object.defineProperty(exports, "CaptchaException", { enumerable: true, get: function () { return exceptions_1.CaptchaException; } });
|
|
28
|
+
Object.defineProperty(exports, "NotFoundException", { enumerable: true, get: function () { return exceptions_1.NotFoundException; } });
|
|
29
|
+
Object.defineProperty(exports, "EmptyResponseException", { enumerable: true, get: function () { return exceptions_1.EmptyResponseException; } });
|
|
30
|
+
Object.defineProperty(exports, "SoundRemovedException", { enumerable: true, get: function () { return exceptions_1.SoundRemovedException; } });
|
|
31
|
+
Object.defineProperty(exports, "InvalidJSONException", { enumerable: true, get: function () { return exceptions_1.InvalidJSONException; } });
|
|
32
|
+
Object.defineProperty(exports, "InvalidResponseException", { enumerable: true, get: function () { return exceptions_1.InvalidResponseException; } });
|
|
33
|
+
var stealth_1 = require("./stealth");
|
|
34
|
+
Object.defineProperty(exports, "stealthAsync", { enumerable: true, get: function () { return stealth_1.stealthAsync; } });
|
|
35
|
+
Object.defineProperty(exports, "StealthConfig", { enumerable: true, get: function () { return stealth_1.StealthConfig; } });
|