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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +80 -0
  4. package/dist/api/comment.d.ts +42 -0
  5. package/dist/api/comment.js +84 -0
  6. package/dist/api/hashtag.d.ts +52 -0
  7. package/dist/api/hashtag.js +118 -0
  8. package/dist/api/playlist.d.ts +53 -0
  9. package/dist/api/playlist.js +112 -0
  10. package/dist/api/search.d.ts +38 -0
  11. package/dist/api/search.js +98 -0
  12. package/dist/api/sound.d.ts +57 -0
  13. package/dist/api/sound.js +133 -0
  14. package/dist/api/trending.d.ts +21 -0
  15. package/dist/api/trending.js +52 -0
  16. package/dist/api/user.d.ts +187 -0
  17. package/dist/api/user.js +498 -0
  18. package/dist/api/video.d.ts +119 -0
  19. package/dist/api/video.js +399 -0
  20. package/dist/exceptions.d.ts +24 -0
  21. package/dist/exceptions.js +61 -0
  22. package/dist/helpers.d.ts +23 -0
  23. package/dist/helpers.js +66 -0
  24. package/dist/index.d.ts +13 -0
  25. package/dist/index.js +35 -0
  26. package/dist/stealth/index.d.ts +70 -0
  27. package/dist/stealth/index.js +128 -0
  28. package/dist/stealth/js/chrome_app.d.ts +1 -0
  29. package/dist/stealth/js/chrome_app.js +27 -0
  30. package/dist/stealth/js/chrome_csi.d.ts +1 -0
  31. package/dist/stealth/js/chrome_csi.js +14 -0
  32. package/dist/stealth/js/chrome_hairline.d.ts +1 -0
  33. package/dist/stealth/js/chrome_hairline.js +16 -0
  34. package/dist/stealth/js/chrome_load_times.d.ts +1 -0
  35. package/dist/stealth/js/chrome_load_times.js +28 -0
  36. package/dist/stealth/js/chrome_runtime_script.d.ts +1 -0
  37. package/dist/stealth/js/chrome_runtime_script.js +84 -0
  38. package/dist/stealth/js/generate_magic_arrays.d.ts +1 -0
  39. package/dist/stealth/js/generate_magic_arrays.js +28 -0
  40. package/dist/stealth/js/iframe_contentWindow.d.ts +1 -0
  41. package/dist/stealth/js/iframe_contentWindow.js +22 -0
  42. package/dist/stealth/js/media_codecs.d.ts +1 -0
  43. package/dist/stealth/js/media_codecs.js +16 -0
  44. package/dist/stealth/js/navigator_hardwareConcurrency.d.ts +1 -0
  45. package/dist/stealth/js/navigator_hardwareConcurrency.js +6 -0
  46. package/dist/stealth/js/navigator_languages.d.ts +1 -0
  47. package/dist/stealth/js/navigator_languages.js +6 -0
  48. package/dist/stealth/js/navigator_permissions.d.ts +1 -0
  49. package/dist/stealth/js/navigator_permissions.js +11 -0
  50. package/dist/stealth/js/navigator_platform.d.ts +1 -0
  51. package/dist/stealth/js/navigator_platform.js +8 -0
  52. package/dist/stealth/js/navigator_plugins_script.d.ts +1 -0
  53. package/dist/stealth/js/navigator_plugins_script.js +37 -0
  54. package/dist/stealth/js/navigator_userAgent_script.d.ts +1 -0
  55. package/dist/stealth/js/navigator_userAgent_script.js +8 -0
  56. package/dist/stealth/js/navigator_vendor_script.d.ts +1 -0
  57. package/dist/stealth/js/navigator_vendor_script.js +6 -0
  58. package/dist/stealth/js/utils_script.d.ts +1 -0
  59. package/dist/stealth/js/utils_script.js +119 -0
  60. package/dist/stealth/js/webgl_vendor_script.d.ts +1 -0
  61. package/dist/stealth/js/webgl_vendor_script.js +16 -0
  62. package/dist/stealth/js/window_outerdimensions.d.ts +1 -0
  63. package/dist/stealth/js/window_outerdimensions.js +9 -0
  64. package/dist/tiktok.d.ts +96 -0
  65. package/dist/tiktok.js +758 -0
  66. package/dist/types.d.ts +58 -0
  67. package/dist/types.js +6 -0
  68. package/package.json +41 -0
package/dist/tiktok.js ADDED
@@ -0,0 +1,758 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // tiktok.ts
4
+ // Mirrors TikTokApi/tiktok.py
5
+ // ============================================================
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.TikTokApi = exports.Logger = void 0;
41
+ const crypto_1 = require("crypto");
42
+ const url_1 = require("url");
43
+ const exceptions_1 = require("./exceptions");
44
+ const helpers_1 = require("./helpers");
45
+ const stealth_1 = require("./stealth");
46
+ const user_1 = require("./api/user");
47
+ const video_1 = require("./api/video");
48
+ const sound_1 = require("./api/sound");
49
+ const hashtag_1 = require("./api/hashtag");
50
+ const comment_1 = require("./api/comment");
51
+ const trending_1 = require("./api/trending");
52
+ const search_1 = require("./api/search");
53
+ const playlist_1 = require("./api/playlist");
54
+ class Logger {
55
+ constructor(name, level = "warn") {
56
+ this._levels = { debug: 0, info: 1, warn: 2, error: 3 };
57
+ this.name = name;
58
+ this.level = level;
59
+ }
60
+ _log(severity, message) {
61
+ if (this._levels[severity] >= this._levels[this.level]) {
62
+ const ts = new Date().toISOString();
63
+ console[severity === "warn" ? "warn" : severity](`${ts} - ${this.name} - ${severity.toUpperCase()} - ${message}`);
64
+ }
65
+ }
66
+ debug(msg) { this._log("debug", msg); }
67
+ info(msg) { this._log("info", msg); }
68
+ warn(msg) { this._log("warn", msg); }
69
+ error(msg) { this._log("error", msg); }
70
+ }
71
+ exports.Logger = Logger;
72
+ // ---------------------------------------------------------------------------
73
+ // TikTokApi
74
+ // ---------------------------------------------------------------------------
75
+ class TikTokApi {
76
+ constructor(options = {}) {
77
+ // ── State ──
78
+ this.sessions = [];
79
+ this.browser = null;
80
+ this.playwright = null;
81
+ this._sessionRecoveryEnabled = true;
82
+ this._sessionCreationLock = false;
83
+ this._cleanupCalled = false;
84
+ this._autoCleanupDeadSessions = true;
85
+ this._playwrightInstance = null;
86
+ this._userAgent = null;
87
+ const { loggingLevel = "warn", loggerName } = options;
88
+ this.logger = new Logger(loggerName ?? "TikTokApi", loggingLevel);
89
+ this.trending = new trending_1.Trending(this);
90
+ this.search = new search_1.Search(this);
91
+ // Wire static parent references
92
+ }
93
+ // ── Factory methods (mirror Python's api.user(), api.video(), etc.) ──
94
+ user(options) {
95
+ return new user_1.User(this, options);
96
+ }
97
+ video(options) {
98
+ return new video_1.Video(this, options);
99
+ }
100
+ sound(options) {
101
+ return new sound_1.Sound(this, options);
102
+ }
103
+ hashtag(options) {
104
+ return new hashtag_1.Hashtag(this, options);
105
+ }
106
+ comment(options) {
107
+ return new comment_1.Comment(this, options.data);
108
+ }
109
+ playlist(options) {
110
+ return new playlist_1.Playlist(this, options);
111
+ }
112
+ // ── Session params ──
113
+ async _setSessionParams(session) {
114
+ const page = session.page;
115
+ // Pass as string expressions so TypeScript never sees browser-only globals
116
+ const userAgent = await page.evaluate("navigator.userAgent");
117
+ const language = await page.evaluate("navigator.language || navigator.userLanguage || 'en'");
118
+ const platform = await page.evaluate("navigator.platform");
119
+ const timezone = await page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone");
120
+ const deviceId = String(BigInt((0, crypto_1.randomInt)(2 ** 30)) * BigInt(2 ** 30) + BigInt((0, crypto_1.randomInt)(2 ** 30)));
121
+ const historyLen = String((0, crypto_1.randomInt)(1, 11));
122
+ const screenHeight = String((0, crypto_1.randomInt)(600, 1081));
123
+ const screenWidth = String((0, crypto_1.randomInt)(800, 1921));
124
+ session.params = {
125
+ aid: "1988",
126
+ app_language: language,
127
+ app_name: "tiktok_web",
128
+ browser_language: language,
129
+ browser_name: "Mozilla",
130
+ browser_online: "true",
131
+ browser_platform: platform,
132
+ browser_version: userAgent,
133
+ channel: "tiktok_web",
134
+ cookie_enabled: "true",
135
+ device_id: deviceId,
136
+ device_platform: "web_pc",
137
+ focus_state: "true",
138
+ from_page: "user",
139
+ history_len: historyLen,
140
+ is_fullscreen: "false",
141
+ is_page_visible: "true",
142
+ language,
143
+ os: platform,
144
+ priority_region: "",
145
+ referer: "",
146
+ region: "US",
147
+ screen_height: screenHeight,
148
+ screen_width: screenWidth,
149
+ tz_name: timezone,
150
+ webcast_language: language,
151
+ };
152
+ }
153
+ // ── Session validation ──
154
+ async _isSessionValid(session) {
155
+ if (!session.isValid)
156
+ return false;
157
+ try {
158
+ // Accessing .url throws if page/context is closed
159
+ void session.page.url();
160
+ return true;
161
+ }
162
+ catch (e) {
163
+ this.logger.warn(`Session validation failed: ${e}`);
164
+ session.isValid = false;
165
+ return false;
166
+ }
167
+ }
168
+ async _markSessionInvalid(session) {
169
+ session.isValid = false;
170
+ try {
171
+ await session.page.close();
172
+ }
173
+ catch (e) {
174
+ this.logger.debug(`Error closing page during invalidation: ${e}`);
175
+ }
176
+ try {
177
+ await session.context.close();
178
+ }
179
+ catch (e) {
180
+ this.logger.debug(`Error closing context during invalidation: ${e}`);
181
+ }
182
+ if (this._autoCleanupDeadSessions) {
183
+ const idx = this.sessions.indexOf(session);
184
+ if (idx !== -1) {
185
+ this.sessions.splice(idx, 1);
186
+ this.logger.debug(`Automatically removed dead session. Remaining: ${this.sessions.length}`);
187
+ }
188
+ }
189
+ }
190
+ async _getValidSessionIndex(kwargs = {}) {
191
+ const maxAttempts = 3;
192
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
193
+ if (kwargs.sessionIndex != null) {
194
+ const i = kwargs.sessionIndex;
195
+ if (i < this.sessions.length) {
196
+ const session = this.sessions[i];
197
+ if (await this._isSessionValid(session))
198
+ return [i, session];
199
+ this.logger.warn(`Requested session ${i} is invalid`);
200
+ }
201
+ }
202
+ else {
203
+ const validSessions = [];
204
+ for (let idx = 0; idx < this.sessions.length; idx++) {
205
+ if (await this._isSessionValid(this.sessions[idx])) {
206
+ validSessions.push([idx, this.sessions[idx]]);
207
+ }
208
+ }
209
+ if (validSessions.length > 0) {
210
+ return validSessions[(0, crypto_1.randomInt)(0, validSessions.length)];
211
+ }
212
+ }
213
+ if (this._sessionRecoveryEnabled && attempt < maxAttempts - 1) {
214
+ this.logger.warn(`No valid sessions found, attempting recovery (attempt ${attempt + 1}/${maxAttempts})`);
215
+ await this._recoverSessions();
216
+ }
217
+ else {
218
+ break;
219
+ }
220
+ }
221
+ throw new Error("No valid sessions available. All sessions appear to be dead. " +
222
+ "Please call createSessions() again.");
223
+ }
224
+ async _recoverSessions() {
225
+ if (this._sessionCreationLock)
226
+ return;
227
+ this._sessionCreationLock = true;
228
+ try {
229
+ this.logger.info("Starting session recovery...");
230
+ const initial = this.sessions.length;
231
+ const validSessions = [];
232
+ for (const s of this.sessions) {
233
+ if (await this._isSessionValid(s))
234
+ validSessions.push(s);
235
+ }
236
+ this.sessions = validSessions;
237
+ const removed = initial - this.sessions.length;
238
+ if (removed > 0)
239
+ this.logger.info(`Removed ${removed} dead session(s)`);
240
+ }
241
+ finally {
242
+ this._sessionCreationLock = false;
243
+ }
244
+ }
245
+ // ── _getSession (deprecated but kept for compat) ──
246
+ _getSession(kwargs = {}) {
247
+ if (this.sessions.length === 0) {
248
+ throw new Error("No sessions created, please create sessions first");
249
+ }
250
+ const i = kwargs.sessionIndex ?? (0, crypto_1.randomInt)(0, this.sessions.length);
251
+ return [i, this.sessions[i]];
252
+ }
253
+ // ── Create sessions ──
254
+ async createSessions(options = {}) {
255
+ const { numSessions = 5, headless = true, msTokens = null, proxies = null, sleepAfter = 1, startingUrl = "https://www.tiktok.com", contextOptions = {}, overrideBrowserArgs = null, cookies = null, suppressResourceLoadTypes = null, browser: browserName = "chromium", executablePath = null, pageFactory = null, browserContextFactory = null, timeout = 30000, enableSessionRecovery = true, allowPartialSessions = false, minSessions = null, } = options;
256
+ this._sessionRecoveryEnabled = enableSessionRecovery;
257
+ // Start Playwright
258
+ const { chromium: pw_chromium, firefox: pw_firefox, webkit: pw_webkit } = await Promise.resolve().then(() => __importStar(require("playwright")));
259
+ if (browserContextFactory) {
260
+ // Custom factory: call it and store the returned context/browser
261
+ const factoryResult = await browserContextFactory(null);
262
+ // browserContextFactory returns a BrowserContext, but we store it as Browser
263
+ // so that _createSession can call newContext() on it — however when
264
+ // browserContextFactory is provided, _createSession skips newContext().
265
+ this.browser = factoryResult;
266
+ }
267
+ else {
268
+ let launchArgs = overrideBrowserArgs ?? undefined;
269
+ let finalHeadless = headless;
270
+ if (browserName === "chromium") {
271
+ if (headless && !overrideBrowserArgs) {
272
+ launchArgs = ["--headless=new"];
273
+ finalHeadless = false;
274
+ }
275
+ this.browser = await pw_chromium.launch({
276
+ headless: finalHeadless,
277
+ args: launchArgs,
278
+ proxy: proxyToPlaywright((0, helpers_1.randomChoice)(proxies)),
279
+ executablePath: executablePath ?? undefined,
280
+ });
281
+ }
282
+ else if (browserName === "firefox") {
283
+ this.browser = await pw_firefox.launch({
284
+ headless: finalHeadless,
285
+ args: launchArgs,
286
+ proxy: proxyToPlaywright((0, helpers_1.randomChoice)(proxies)),
287
+ executablePath: executablePath ?? undefined,
288
+ });
289
+ }
290
+ else if (browserName === "webkit") {
291
+ this.browser = await pw_webkit.launch({
292
+ headless: finalHeadless,
293
+ args: launchArgs,
294
+ proxy: proxyToPlaywright((0, helpers_1.randomChoice)(proxies)),
295
+ executablePath: executablePath ?? undefined,
296
+ });
297
+ }
298
+ else {
299
+ throw new Error("Invalid browser argument. Use 'chromium', 'firefox', or 'webkit'.");
300
+ }
301
+ }
302
+ // Detect dynamic User-Agent to avoid Chrome version mismatches in headless mode
303
+ let resolvedUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
304
+ if (this.browser && browserName === "chromium") {
305
+ try {
306
+ const tempContext = await this.browser.newContext();
307
+ const tempPage = await tempContext.newPage();
308
+ const rawUA = await tempPage.evaluate("navigator.userAgent");
309
+ resolvedUA = rawUA.replace("HeadlessChrome", "Chrome");
310
+ await tempPage.close();
311
+ await tempContext.close();
312
+ }
313
+ catch (e) {
314
+ // Use hardcoded fallback
315
+ }
316
+ }
317
+ this._userAgent = resolvedUA;
318
+ const createOne = () => this._createSession({
319
+ url: startingUrl,
320
+ msToken: (0, helpers_1.randomChoice)(msTokens),
321
+ proxy: (0, helpers_1.randomChoice)(proxies),
322
+ contextOptions,
323
+ sleepAfter,
324
+ cookies: (0, helpers_1.randomChoice)(cookies),
325
+ suppressResourceLoadTypes,
326
+ timeout,
327
+ pageFactory,
328
+ browserContextFactory,
329
+ });
330
+ if (allowPartialSessions) {
331
+ const results = await Promise.allSettled(Array.from({ length: numSessions }, createOne));
332
+ const failed = results.filter((r) => r.status === "rejected").length;
333
+ const succeeded = this.sessions.length;
334
+ const minRequired = minSessions ?? 1;
335
+ if (succeeded < minRequired) {
336
+ const errors = results
337
+ .filter((r) => r.status === "rejected")
338
+ .slice(0, 3)
339
+ .map((r) => String(r.reason));
340
+ throw new Error(`Failed to create minimum required sessions. Created ${succeeded}/${numSessions}, needed ${minRequired}.\n` +
341
+ `Errors: ${errors.join("; ")}`);
342
+ }
343
+ if (failed > 0) {
344
+ this.logger.warn(`Created ${succeeded}/${numSessions} sessions. ${failed} failed.`);
345
+ }
346
+ }
347
+ else {
348
+ await Promise.all(Array.from({ length: numSessions }, createOne));
349
+ }
350
+ }
351
+ async _createSession(options) {
352
+ const { url = "https://www.tiktok.com", msToken = null, proxy, contextOptions = {}, sleepAfter = 1, suppressResourceLoadTypes = null, timeout = 30000, pageFactory = null, } = options;
353
+ let { cookies = null } = options;
354
+ let context;
355
+ let page;
356
+ try {
357
+ if (msToken != null) {
358
+ cookies = cookies ?? {};
359
+ cookies["msToken"] = msToken;
360
+ }
361
+ let defaultUA = this._userAgent;
362
+ if (!defaultUA) {
363
+ defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
364
+ }
365
+ context = await this.browser.newContext({
366
+ proxy: proxyToPlaywright(proxy),
367
+ userAgent: defaultUA,
368
+ ...contextOptions,
369
+ });
370
+ if (cookies) {
371
+ const hostname = new url_1.URL(url).hostname;
372
+ const formattedCookies = Object.entries(cookies)
373
+ .filter(([, v]) => v != null)
374
+ .map(([name, value]) => ({
375
+ name,
376
+ value,
377
+ domain: hostname,
378
+ path: "/",
379
+ }));
380
+ await context.addCookies(formattedCookies);
381
+ }
382
+ const applySuppression = async (p) => {
383
+ if (!suppressResourceLoadTypes)
384
+ return;
385
+ await p.route("**/*", (route, request) => {
386
+ if (suppressResourceLoadTypes.includes(request.resourceType())) {
387
+ void route.abort();
388
+ }
389
+ else {
390
+ void route.continue();
391
+ }
392
+ });
393
+ };
394
+ if (pageFactory) {
395
+ page = await pageFactory(context);
396
+ }
397
+ else {
398
+ page = await context.newPage();
399
+ await (0, stealth_1.stealthAsync)(page); // apply anti-bot stealth scripts
400
+ await applySuppression(page);
401
+ await page.goto(url);
402
+ }
403
+ if (!page.url().includes("tiktok")) {
404
+ await page.goto("https://www.tiktok.com");
405
+ }
406
+ let requestHeaders = null;
407
+ page.once("request", (request) => {
408
+ requestHeaders = request.headers();
409
+ });
410
+ if (pageFactory) {
411
+ await applySuppression(page);
412
+ }
413
+ page.setDefaultNavigationTimeout(timeout);
414
+ // Simulate scrolling to avoid bot detection
415
+ const x = (0, crypto_1.randomInt)(0, 51);
416
+ const y = (0, crypto_1.randomInt)(0, 51);
417
+ const a = (0, crypto_1.randomInt)(1, 51);
418
+ const b = (0, crypto_1.randomInt)(100, 201);
419
+ await page.mouse.move(x, y);
420
+ try {
421
+ await page.waitForLoadState("networkidle", { timeout: 15000 });
422
+ }
423
+ catch (e) {
424
+ this.logger.debug(`networkidle timeout during session creation, continuing...`);
425
+ }
426
+ await page.mouse.move(a, b);
427
+ const session = {
428
+ context,
429
+ page,
430
+ msToken,
431
+ proxy: proxy,
432
+ headers: requestHeaders,
433
+ baseUrl: url,
434
+ isValid: true,
435
+ };
436
+ let finalMsToken = msToken;
437
+ if (finalMsToken == null) {
438
+ await (0, helpers_1.sleep)(sleepAfter * 1000);
439
+ const sessionCookies = await this.getSessionCookies(session);
440
+ finalMsToken = sessionCookies["msToken"] ?? null;
441
+ session.msToken = finalMsToken;
442
+ if (!finalMsToken) {
443
+ this.logger.info(`Failed to get msToken on session index ${this.sessions.length}, consider specifying ms_tokens`);
444
+ }
445
+ }
446
+ this.sessions.push(session);
447
+ await this._setSessionParams(session);
448
+ }
449
+ catch (e) {
450
+ this.logger.error(`Failed to create session: ${e}`);
451
+ throw e;
452
+ }
453
+ }
454
+ // ── Cookie helpers ──
455
+ async setSessionCookies(session, cookies) {
456
+ // Cast via unknown to satisfy Playwright's strict cookie type
457
+ await session.context.addCookies(cookies);
458
+ }
459
+ async getSessionCookies(session) {
460
+ const cookies = await session.context.cookies();
461
+ return Object.fromEntries(cookies.map((c) => [c.name, c.value]));
462
+ }
463
+ // ── JS fetch / XBogus / Sign ──
464
+ async runFetchScript(url, headers, kwargs = {}) {
465
+ let session;
466
+ try {
467
+ [, session] = await this._getValidSessionIndex(kwargs);
468
+ }
469
+ catch {
470
+ [, session] = this._getSession(kwargs);
471
+ }
472
+ try {
473
+ return (await session.page.evaluate(async ({ fetchUrl, fetchHeaders }) => {
474
+ const response = await fetch(fetchUrl, { method: 'GET', headers: fetchHeaders });
475
+ return await response.text();
476
+ }, { fetchUrl: url, fetchHeaders: headers }));
477
+ }
478
+ catch (e) {
479
+ this.logger.error(`Session failed during fetch: ${e}`);
480
+ await this._markSessionInvalid(session);
481
+ throw e;
482
+ }
483
+ }
484
+ async generateXBogus(url, kwargs = {}) {
485
+ let session;
486
+ try {
487
+ [, session] = await this._getValidSessionIndex(kwargs);
488
+ }
489
+ catch {
490
+ [, session] = this._getSession(kwargs);
491
+ }
492
+ const maxAttempts = 5;
493
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
494
+ try {
495
+ const timeoutMs = (0, crypto_1.randomInt)(5000, 20001);
496
+ await session.page.waitForFunction("typeof window !== 'undefined' && window.byted_acrawler !== undefined", { timeout: timeoutMs });
497
+ break;
498
+ }
499
+ catch (e) {
500
+ if (attempt === maxAttempts - 1) {
501
+ // eslint-disable-next-line preserve-caught-error
502
+ throw new Error(`Failed to load tiktok after ${maxAttempts} attempts, consider using a proxy`);
503
+ }
504
+ const tryUrls = [
505
+ "https://www.tiktok.com/foryou",
506
+ "https://www.tiktok.com",
507
+ "https://www.tiktok.com/@tiktok",
508
+ ];
509
+ await session.page.goto(tryUrls[(0, crypto_1.randomInt)(0, tryUrls.length)]);
510
+ }
511
+ }
512
+ try {
513
+ const result = await session.page.evaluate(`window.byted_acrawler.frontierSign("${url}")`);
514
+ return result;
515
+ }
516
+ catch (e) {
517
+ this.logger.error(`Session died during x-bogus evaluation: ${e}`);
518
+ await this._markSessionInvalid(session);
519
+ throw e;
520
+ }
521
+ }
522
+ async signUrl(url, kwargs = {}) {
523
+ let i;
524
+ let session;
525
+ try {
526
+ [i, session] = await this._getValidSessionIndex(kwargs);
527
+ }
528
+ catch {
529
+ [i, session] = this._getSession(kwargs);
530
+ }
531
+ const xBogus = (await this.generateXBogus(url, { sessionIndex: i }))["X-Bogus"];
532
+ if (!xBogus)
533
+ throw new Error("Failed to generate X-Bogus");
534
+ return url + (url.includes("?") ? "&" : "?") + `X-Bogus=${xBogus}`;
535
+ }
536
+ // ── Session Storage ──
537
+ /**
538
+ * Saves the current Playwright browser context state (cookies, local storage) to a file.
539
+ * You can load this state back by passing \`contextOptions: { storageState: "path.json" }\` to \`createSessions\`.
540
+ */
541
+ async saveSessionState(path, sessionIndex = 0) {
542
+ if (this.sessions.length <= sessionIndex) {
543
+ throw new Error(`Session index ${sessionIndex} does not exist`);
544
+ }
545
+ const session = this.sessions[sessionIndex];
546
+ if (session.context) {
547
+ await session.context.storageState({ path });
548
+ this.logger.info(`Session state saved to ${path}`);
549
+ }
550
+ }
551
+ // ── makeRequest ──
552
+ async makeRequest(options) {
553
+ const { url, headers: extraHeaders = null, params: extraParams = null, retries = 3, exponentialBackoff = true, sessionIndex, } = options;
554
+ let i;
555
+ let session;
556
+ try {
557
+ [i, session] = await this._getValidSessionIndex({ sessionIndex });
558
+ }
559
+ catch {
560
+ [i, session] = this._getSession({ sessionIndex });
561
+ }
562
+ // Python: if session.params is not None: params = {**session.params, **params}
563
+ // Always merge — extraParams may be null/undefined
564
+ const params = {
565
+ ...(session.params ?? {}),
566
+ ...(extraParams ?? {}),
567
+ };
568
+ // Python: if headers is not None: headers = {**session.headers, **headers} else: headers = session.headers
569
+ const headers = extraHeaders
570
+ ? { ...(session.headers ?? {}), ...extraHeaders }
571
+ : { ...(session.headers ?? {}) };
572
+ // Ensure msToken
573
+ if (!params["msToken"]) {
574
+ if (session.msToken) {
575
+ params["msToken"] = session.msToken;
576
+ }
577
+ else {
578
+ const cookieMap = await this.getSessionCookies(session);
579
+ const msTok = cookieMap["msToken"];
580
+ if (!msTok) {
581
+ // Python uses self.logger.warn (same as .warning in Python logging)
582
+ this.logger.warn("Failed to get msToken from cookies, trying to make the request anyway (probably will fail)");
583
+ }
584
+ params["msToken"] = msTok;
585
+ }
586
+ }
587
+ const encodedParams = new URLSearchParams(Object.fromEntries(Object.entries(params)
588
+ .filter(([, v]) => v != null)
589
+ .map(([k, v]) => [k, String(v)]))).toString();
590
+ const fullUrl = `${url}?${encodedParams}`;
591
+ const signedUrl = await this.signUrl(fullUrl, { sessionIndex: i });
592
+ let retryCount = 0;
593
+ while (retryCount < retries) {
594
+ retryCount++;
595
+ try {
596
+ const result = await this.runFetchScript(signedUrl, headers, { sessionIndex: i });
597
+ if (result == null)
598
+ throw new Error("runFetchScript returned null");
599
+ if (result === "") {
600
+ throw new exceptions_1.EmptyResponseException(result, "TikTok returned an empty response. They are detecting you're a bot. " +
601
+ "Try: headless=false, browser='webkit', or a proxy.");
602
+ }
603
+ try {
604
+ const data = JSON.parse(result);
605
+ if (data["status_code"] !== 0) {
606
+ this.logger.error(`Got unexpected status code: ${JSON.stringify(data)}`);
607
+ }
608
+ return data;
609
+ }
610
+ catch {
611
+ if (retryCount === retries) {
612
+ this.logger.error(`Failed to decode JSON response: ${result}`);
613
+ throw new exceptions_1.InvalidJSONException(result);
614
+ }
615
+ this.logger.info(`Failed a request, retrying (${retryCount}/${retries})`);
616
+ if (exponentialBackoff) {
617
+ await (0, helpers_1.sleep)(2 ** retryCount * 1000);
618
+ }
619
+ else {
620
+ await (0, helpers_1.sleep)(1000);
621
+ }
622
+ }
623
+ }
624
+ catch (e) {
625
+ if (e instanceof exceptions_1.EmptyResponseException || e instanceof exceptions_1.InvalidJSONException)
626
+ throw e;
627
+ this.logger.error(`Playwright error during request: ${e}`);
628
+ await this._markSessionInvalid(session);
629
+ if (retryCount < retries) {
630
+ this.logger.info(`Retrying with a new session (${retryCount}/${retries})`);
631
+ try {
632
+ [i, session] = await this._getValidSessionIndex({ sessionIndex });
633
+ }
634
+ catch (sessionErr) {
635
+ this.logger.error(`Failed to get valid session: ${sessionErr}`);
636
+ throw sessionErr;
637
+ }
638
+ }
639
+ else {
640
+ throw e;
641
+ }
642
+ }
643
+ }
644
+ throw new Error("makeRequest: exhausted all retries");
645
+ }
646
+ // ── Close / cleanup ──
647
+ async closeSessions() {
648
+ this.logger.debug(`Closing ${this.sessions.length} sessions...`);
649
+ for (const session of this.sessions) {
650
+ try {
651
+ await session.page.close();
652
+ }
653
+ catch (e) {
654
+ this.logger.debug(`Error closing page: ${e}`);
655
+ }
656
+ try {
657
+ await session.context.close();
658
+ }
659
+ catch (e) {
660
+ this.logger.debug(`Error closing context: ${e}`);
661
+ }
662
+ }
663
+ this.sessions = [];
664
+ try {
665
+ if (this.browser) {
666
+ await this.browser.close();
667
+ this.browser = null;
668
+ }
669
+ }
670
+ catch (e) {
671
+ this.logger.debug(`Error closing browser: ${e}`);
672
+ }
673
+ this._cleanupCalled = true;
674
+ this.logger.debug("All sessions and browser resources closed successfully");
675
+ }
676
+ async stopPlaywright() {
677
+ // Python also calls self.playwright.stop() — we store it on this.playwright
678
+ try {
679
+ if (this.browser) {
680
+ await this.browser.close();
681
+ this.browser = null;
682
+ }
683
+ }
684
+ catch (e) {
685
+ this.logger.debug(`Error closing browser: ${e}`);
686
+ }
687
+ // Python also stops playwright instance (equivalent of playwright.stop())
688
+ // We don't hold the playwright instance separately, browser.close() covers it.
689
+ }
690
+ async getSessionContent(url, kwargs = {}) {
691
+ let session;
692
+ try {
693
+ [, session] = await this._getValidSessionIndex(kwargs);
694
+ }
695
+ catch {
696
+ [, session] = this._getSession(kwargs);
697
+ }
698
+ try {
699
+ return await session.page.content();
700
+ }
701
+ catch (e) {
702
+ this.logger.error(`Session died during getSessionContent: ${e}`);
703
+ await this._markSessionInvalid(session);
704
+ throw e;
705
+ }
706
+ }
707
+ // ── Resource stats / health ──
708
+ getResourceStats() {
709
+ const validSessions = this.sessions.filter((s) => s.isValid).length;
710
+ return {
711
+ totalSessions: this.sessions.length,
712
+ validSessions,
713
+ invalidSessions: this.sessions.length - validSessions,
714
+ hasBrowser: this.browser != null,
715
+ hasPlaywright: false,
716
+ cleanupCalled: this._cleanupCalled,
717
+ autoCleanupEnabled: this._autoCleanupDeadSessions,
718
+ recoveryEnabled: this._sessionRecoveryEnabled,
719
+ };
720
+ }
721
+ async healthCheck() {
722
+ const health = this.getResourceStats();
723
+ const sessionDetails = await Promise.all(this.sessions.map(async (s, i) => ({
724
+ index: i,
725
+ valid: await this._isSessionValid(s),
726
+ markedValid: s.isValid,
727
+ })));
728
+ health.sessionDetails = sessionDetails;
729
+ health.healthySessions = sessionDetails.filter((s) => s.valid).length;
730
+ if (health.invalidSessions > 0 && !this._autoCleanupDeadSessions) {
731
+ health.warning = `${health.invalidSessions} invalid sessions accumulating (auto-cleanup disabled)`;
732
+ }
733
+ return health;
734
+ }
735
+ // ── Context manager (using/Symbol.asyncDispose) ──
736
+ async [Symbol.asyncDispose]() {
737
+ await this.closeSessions();
738
+ }
739
+ }
740
+ exports.TikTokApi = TikTokApi;
741
+ // ---------------------------------------------------------------------------
742
+ // Helper: convert proxy string or object to Playwright proxy format
743
+ // ---------------------------------------------------------------------------
744
+ function proxyToPlaywright(proxy) {
745
+ if (!proxy)
746
+ return undefined;
747
+ if (typeof proxy === "string")
748
+ return { server: proxy };
749
+ if (typeof proxy === "object") {
750
+ const p = proxy;
751
+ return {
752
+ server: p["server"] ?? p["http"] ?? p["https"] ?? "",
753
+ username: p["username"],
754
+ password: p["password"],
755
+ };
756
+ }
757
+ return undefined;
758
+ }