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
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
|
+
}
|