ani-auto 1.4.4 → 2.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/anime/LICENSE +21 -0
- package/anime/api/index.js +110 -0
- package/anime/app.js +79 -0
- package/anime/controllers/animeInfoController.js +48 -0
- package/anime/controllers/animeListController.js +21 -0
- package/anime/controllers/homeController.js +42 -0
- package/anime/controllers/playController.js +140 -0
- package/anime/controllers/queueController.js +20 -0
- package/anime/controllers/testController.js +667 -0
- package/anime/index.js +139 -0
- package/anime/middleware/cache.js +64 -0
- package/anime/middleware/errorHandler.js +33 -0
- package/anime/middleware/rateLimiter.js +73 -0
- package/anime/models/animeInfoModel.js +193 -0
- package/anime/models/animeListModel.js +69 -0
- package/anime/models/homeModel.js +33 -0
- package/anime/models/playModel.js +528 -0
- package/anime/models/queueModel.js +22 -0
- package/anime/package.json +27 -0
- package/anime/pnpm-lock.yaml +1774 -0
- package/anime/readme.md +236 -0
- package/anime/routes/animeInfoRoutes.js +9 -0
- package/anime/routes/animeListRoutes.js +9 -0
- package/anime/routes/homeRoutes.js +10 -0
- package/anime/routes/playRoutes.js +11 -0
- package/anime/routes/queueRoutes.js +8 -0
- package/anime/routes/testRoutes.js +325 -0
- package/anime/routes/webhookRoutes.js +23 -0
- package/anime/scrapers/animepahe.js +1053 -0
- package/anime/test-server.js +10 -0
- package/anime/test.sh +275 -0
- package/anime/utils/browser.js +172 -0
- package/anime/utils/config.js +209 -0
- package/anime/utils/dataProcessor.js +172 -0
- package/anime/utils/diskCache.js +228 -0
- package/anime/utils/flaresolverr.js +361 -0
- package/anime/utils/jsParser.js +19 -0
- package/anime/utils/redis.js +64 -0
- package/anime/utils/requestManager.js +706 -0
- package/anime/utils/urlConverter.js +88 -0
- package/anime/utils/webhookNotifier.js +190 -0
- package/cookiereader.py +291 -0
- package/package.json +14 -6
- package/src/anilist.js +15 -2
- package/src/api/anilist.js +32 -0
- package/src/api/anime.js +181 -0
- package/src/api/cf-bypass.js +131 -0
- package/src/api/config.js +134 -0
- package/src/api/daemon.js +62 -0
- package/src/api/download.js +98 -0
- package/src/api/match.js +58 -0
- package/src/api/runs.js +15 -0
- package/src/api/update.js +96 -0
- package/src/cli.js +18 -2
- package/src/config.js +14 -2
- package/src/daemon.js +1 -1
- package/src/db.js +38 -11
- package/src/engine.js +62 -11
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -4
- package/web/index.html +13 -0
- package/web/package-lock.json +1420 -0
- package/web/package.json +24 -0
- package/web/src/App.vue +200 -0
- package/web/src/api/client.js +75 -0
- package/web/src/assets/styles/base.css +961 -0
- package/web/src/components/UpdateModal.vue +157 -0
- package/web/src/components/sidebar/SidebarResizable.vue +170 -0
- package/web/src/components/sidebar/sidebarConfig.js +24 -0
- package/web/src/main.js +104 -0
- package/web/src/router/index.js +44 -0
- package/web/src/stores/config.js +27 -0
- package/web/src/stores/download.js +107 -0
- package/web/src/stores/update.js +73 -0
- package/web/src/views/About.vue +42 -0
- package/web/src/views/AniListSync.vue +122 -0
- package/web/src/views/AnimeDetail.vue +202 -0
- package/web/src/views/AnimeList.vue +110 -0
- package/web/src/views/CFResult.vue +312 -0
- package/web/src/views/DaemonControl.vue +83 -0
- package/web/src/views/Dashboard.vue +187 -0
- package/web/src/views/DownloadCenter.vue +153 -0
- package/web/src/views/MatchFinder.vue +131 -0
- package/web/src/views/OAuthCallback.vue +44 -0
- package/web/src/views/Onboarding.vue +93 -0
- package/web/src/views/RunHistory.vue +122 -0
- package/web/src/views/Settings.vue +410 -0
- package/web/vite.config.js +23 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
const { launchBrowser } = require("./browser");
|
|
2
|
+
const cloudscraper = require("cloudscraper");
|
|
3
|
+
const axios = require("axios");
|
|
4
|
+
const { execFile } = require("child_process");
|
|
5
|
+
const Config = require("./config");
|
|
6
|
+
const flaresolverr = require("./flaresolverr");
|
|
7
|
+
const { CustomError } = require("../middleware/errorHandler");
|
|
8
|
+
|
|
9
|
+
class RequestManager {
|
|
10
|
+
/**
|
|
11
|
+
* Domains accessible directly from local machine (no FlareSolverr needed)
|
|
12
|
+
*/
|
|
13
|
+
static LOCAL_DOMAINS = ["kwik", "uwucdn", "uwu", "owocdn"];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a URL should be fetched directly (not through FlareSolverr)
|
|
17
|
+
*/
|
|
18
|
+
static _isLocalDomain(url) {
|
|
19
|
+
try {
|
|
20
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
21
|
+
return this.LOCAL_DOMAINS.some(d => host.includes(d));
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute a request via system curl with cf_clearance cookie.
|
|
29
|
+
* Matches KevCui's proven approach: plain curl + cf_clearance + UA + --compressed.
|
|
30
|
+
*/
|
|
31
|
+
static _curlGet(url, options = {}) {
|
|
32
|
+
const { cfClearance, userAgent, referer, timeout = 15000 } = options;
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const args = ['-sS', '-L', '--compressed'];
|
|
36
|
+
|
|
37
|
+
if (cfClearance) {
|
|
38
|
+
args.push('-b', `cf_clearance=${cfClearance}`);
|
|
39
|
+
}
|
|
40
|
+
if (userAgent) {
|
|
41
|
+
args.push('-A', userAgent);
|
|
42
|
+
}
|
|
43
|
+
if (referer) {
|
|
44
|
+
args.push('-e', referer);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
args.push('-w', '\n%{http_code}');
|
|
48
|
+
args.push(url);
|
|
49
|
+
|
|
50
|
+
execFile('curl', args, { timeout, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
return reject(new Error(`curl failed: ${err.message}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines = stdout.trim().split('\n');
|
|
56
|
+
const statusCode = parseInt(lines.pop(), 10);
|
|
57
|
+
const body = lines.join('\n');
|
|
58
|
+
|
|
59
|
+
resolve({ body, status: statusCode });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Universal cloudscraper method - handles GET, POST, and any HTTP method
|
|
66
|
+
* @param {Object} options - Request options
|
|
67
|
+
* @param {string} options.method - HTTP method (GET, POST, etc.)
|
|
68
|
+
* @param {string} options.url - Target URL
|
|
69
|
+
* @param {Object} options.headers - Custom headers
|
|
70
|
+
* @param {Object} options.form - Form data (for POST)
|
|
71
|
+
* @param {Object} options.json - JSON body (for POST)
|
|
72
|
+
* @param {boolean} options.followRedirect - Follow redirects (default: true)
|
|
73
|
+
* @param {number} options.timeout - Request timeout in ms
|
|
74
|
+
* @param {string} options.referer - Referer header
|
|
75
|
+
* @returns {Promise<Object>} Response with { statusCode, headers, body }
|
|
76
|
+
*/
|
|
77
|
+
static async cloudscraperRequest(options = {}) {
|
|
78
|
+
const {
|
|
79
|
+
method = "GET",
|
|
80
|
+
url,
|
|
81
|
+
headers = {},
|
|
82
|
+
form = null,
|
|
83
|
+
json = null,
|
|
84
|
+
encoding,
|
|
85
|
+
followRedirect = true,
|
|
86
|
+
followAllRedirects = false,
|
|
87
|
+
timeout = 8000,
|
|
88
|
+
referer = Config.getUrl("home"),
|
|
89
|
+
resolveWithFullResponse = true,
|
|
90
|
+
simple = false,
|
|
91
|
+
} = options;
|
|
92
|
+
|
|
93
|
+
if (!url) {
|
|
94
|
+
throw new CustomError("URL is required for cloudscraper request", 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (flaresolverr.isEnabled() && !this._isLocalDomain(url)) {
|
|
98
|
+
const result =
|
|
99
|
+
(method || "GET").toUpperCase() === "POST"
|
|
100
|
+
? await flaresolverr.post(url, form || json || {})
|
|
101
|
+
: await flaresolverr.get(url);
|
|
102
|
+
return {
|
|
103
|
+
statusCode: result.status,
|
|
104
|
+
headers: {},
|
|
105
|
+
body: result.body,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const defaultHeaders = {
|
|
110
|
+
"User-Agent": Config.userAgent,
|
|
111
|
+
Accept:
|
|
112
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
113
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
114
|
+
Referer: referer,
|
|
115
|
+
Connection: "keep-alive",
|
|
116
|
+
"Upgrade-Insecure-Requests": "1",
|
|
117
|
+
DNT: "1",
|
|
118
|
+
"Sec-Fetch-Dest": "document",
|
|
119
|
+
"Sec-Fetch-Mode": "navigate",
|
|
120
|
+
"Sec-Fetch-Site": "cross-site",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const requestOptions = {
|
|
124
|
+
method,
|
|
125
|
+
uri: url,
|
|
126
|
+
headers: { ...defaultHeaders, ...headers },
|
|
127
|
+
followRedirect,
|
|
128
|
+
followAllRedirects,
|
|
129
|
+
simple,
|
|
130
|
+
resolveWithFullResponse,
|
|
131
|
+
timeout,
|
|
132
|
+
};
|
|
133
|
+
if (encoding !== undefined) {
|
|
134
|
+
requestOptions.encoding = encoding;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add body data if provided
|
|
138
|
+
if (form) {
|
|
139
|
+
requestOptions.form = form;
|
|
140
|
+
requestOptions.headers["Content-Type"] =
|
|
141
|
+
"application/x-www-form-urlencoded";
|
|
142
|
+
} else if (json) {
|
|
143
|
+
requestOptions.json = json;
|
|
144
|
+
requestOptions.headers["Content-Type"] = "application/json";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
console.log(`[Cloudscraper] ${method} ${url}`);
|
|
149
|
+
const response = await cloudscraper(requestOptions);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
statusCode: response.statusCode,
|
|
153
|
+
headers: response.headers,
|
|
154
|
+
body: response.body,
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Handle redirects as responses (not errors)
|
|
158
|
+
if (error.statusCode === 301 || error.statusCode === 302) {
|
|
159
|
+
return {
|
|
160
|
+
statusCode: error.statusCode,
|
|
161
|
+
headers: error.response?.headers || {},
|
|
162
|
+
body: error.response?.body || "",
|
|
163
|
+
location: error.response?.headers?.location,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.error(`[Cloudscraper Error] ${method} ${url}:`, error.message);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Simplified GET request with cloudscraper
|
|
174
|
+
*/
|
|
175
|
+
static async cloudscraperGet(url, options = {}) {
|
|
176
|
+
return this.cloudscraperRequest({
|
|
177
|
+
method: "GET",
|
|
178
|
+
url,
|
|
179
|
+
...options,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Simplified POST request with cloudscraper
|
|
185
|
+
*/
|
|
186
|
+
static async cloudscraperPost(url, data = {}, options = {}) {
|
|
187
|
+
const isJson = options.json !== false;
|
|
188
|
+
|
|
189
|
+
return this.cloudscraperRequest({
|
|
190
|
+
method: "POST",
|
|
191
|
+
url,
|
|
192
|
+
...(isJson ? { json: data } : { form: data }),
|
|
193
|
+
...options,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static async fetch(url, cookieHeader, type = "default") {
|
|
198
|
+
if (type === "default") {
|
|
199
|
+
return this.fetchApiData(url, {}, cookieHeader);
|
|
200
|
+
} else if (type === "heavy") {
|
|
201
|
+
return this.scrapeWithPlaywright(url);
|
|
202
|
+
} else {
|
|
203
|
+
console.trace(
|
|
204
|
+
'Invalid fetch type specified. Please use "json", "heavy", or "default".',
|
|
205
|
+
);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetch a play page: try cookies + axios first, fall back to FlareSolverr.
|
|
212
|
+
*
|
|
213
|
+
* WHY THIS ORDER:
|
|
214
|
+
* FlareSolverr launches a full Chrome browser per request (~0.5–2s each).
|
|
215
|
+
* Cloudflare rate-limits after ~5 rapid browser-based requests to
|
|
216
|
+
* animepahe.pw/play/..., causing subsequent episodes to fail.
|
|
217
|
+
*
|
|
218
|
+
* Cookies + axios (~0.5s) don't trigger Turnstile, so we try that FIRST.
|
|
219
|
+
* Only when cookies fail (expired/missing) do we call FlareSolverr,
|
|
220
|
+
* then save the fresh cookies + User-Agent for next time.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} url - Target URL on animepahe.pw
|
|
223
|
+
* @returns {Promise<string>} HTML response body
|
|
224
|
+
*/
|
|
225
|
+
static async fetchWithCookies(url) {
|
|
226
|
+
if (Config.cfClearance) {
|
|
227
|
+
try {
|
|
228
|
+
const { body, status } = await this._curlGet(url, {
|
|
229
|
+
cfClearance: Config.cfClearance,
|
|
230
|
+
userAgent: Config.userAgent,
|
|
231
|
+
referer: Config.getUrl("home"),
|
|
232
|
+
timeout: 15000,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
body &&
|
|
237
|
+
body.length > 100 &&
|
|
238
|
+
!body.toLowerCase().includes("just a moment") &&
|
|
239
|
+
!body.toLowerCase().includes("checking your browser") &&
|
|
240
|
+
!body.toLowerCase().includes("ddos protection by cloudflare")
|
|
241
|
+
) {
|
|
242
|
+
return body;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(
|
|
246
|
+
"[fetchWithCookies] cf_clearance returned challenge page, falling back to FlareSolverr",
|
|
247
|
+
);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.log(
|
|
250
|
+
`[fetchWithCookies] curl request: ${err.message}, falling back to FlareSolverr`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (flaresolverr.isEnabled()) {
|
|
256
|
+
const result = await flaresolverr.get(url);
|
|
257
|
+
this._saveFlareResult(result);
|
|
258
|
+
return result.body;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new CustomError(
|
|
262
|
+
Config.cfClearance
|
|
263
|
+
? "Cookies failed and FlareSolverr not enabled"
|
|
264
|
+
: "No cf_clearance available",
|
|
265
|
+
503,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save cf_clearance and User-Agent from a FlareSolverr result.
|
|
271
|
+
*/
|
|
272
|
+
static _saveFlareResult(result) {
|
|
273
|
+
if (result.cookies && result.cookies.length > 0) {
|
|
274
|
+
const cf = result.cookies.find((c) => c.name === "cf_clearance");
|
|
275
|
+
if (cf && cf.value) {
|
|
276
|
+
Config.setCfClearance(cf.value);
|
|
277
|
+
console.log("[_saveFlareResult] Saved cf_clearance from FlareSolverr");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (result.userAgent) {
|
|
281
|
+
Config.userAgent = result.userAgent;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Legacy method - now uses the universal cloudscraper method
|
|
287
|
+
*/
|
|
288
|
+
static async scrapeWithCloudScraper(url, options = {}) {
|
|
289
|
+
console.log(`Fetching HTML from ${url}...`);
|
|
290
|
+
|
|
291
|
+
if (flaresolverr.isEnabled()) {
|
|
292
|
+
const { body } = await flaresolverr.get(url);
|
|
293
|
+
return body;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const response = await this.cloudscraperGet(url, {
|
|
297
|
+
headers: {
|
|
298
|
+
Referer: Config.baseUrl,
|
|
299
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
300
|
+
dnt: "1",
|
|
301
|
+
"sec-ch-ua":
|
|
302
|
+
'"Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"',
|
|
303
|
+
"sec-ch-ua-mobile": "?0",
|
|
304
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
305
|
+
"sec-fetch-dest": "empty",
|
|
306
|
+
"sec-fetch-mode": "cors",
|
|
307
|
+
"sec-fetch-site": "same-origin",
|
|
308
|
+
"x-requested-with": "XMLHttpRequest",
|
|
309
|
+
...options.headers,
|
|
310
|
+
},
|
|
311
|
+
timeout: options.timeout || 20000,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return response.body;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static async scrapeWithPlaywright(url) {
|
|
318
|
+
console.log("Fetching content from:", url);
|
|
319
|
+
|
|
320
|
+
if (flaresolverr.isEnabled()) {
|
|
321
|
+
const { body } = await flaresolverr.get(url);
|
|
322
|
+
return body;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const proxy = Config.proxyEnabled ? Config.getRandomProxy() : null;
|
|
326
|
+
console.log(`Using proxy: ${proxy || "none"}`);
|
|
327
|
+
|
|
328
|
+
const GLOBAL_TIMEOUT = 120000; // 120s total for Playwright operations (DDoS challenges can be slow)
|
|
329
|
+
|
|
330
|
+
const browser = await launchBrowser();
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const contextOptions = {};
|
|
334
|
+
|
|
335
|
+
if (proxy) {
|
|
336
|
+
contextOptions.proxy = { server: proxy };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const context = await browser.newContext(contextOptions);
|
|
340
|
+
const page = await context.newPage();
|
|
341
|
+
|
|
342
|
+
// Stealth measures
|
|
343
|
+
await page.addInitScript(() => {
|
|
344
|
+
delete navigator.__proto__.webdriver;
|
|
345
|
+
Object.defineProperty(navigator, "plugins", {
|
|
346
|
+
get: () => [1, 2, 3],
|
|
347
|
+
});
|
|
348
|
+
Object.defineProperty(navigator, "languages", {
|
|
349
|
+
get: () => ["en-US", "en"],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const originalQuery = window.navigator.permissions.query;
|
|
353
|
+
window.navigator.permissions.query = (parameters) =>
|
|
354
|
+
parameters.name === "notifications"
|
|
355
|
+
? Promise.resolve({ state: Notification.permission })
|
|
356
|
+
: originalQuery(parameters);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Realistic headers
|
|
360
|
+
await page.setExtraHTTPHeaders({
|
|
361
|
+
"User-Agent": Config.userAgent,
|
|
362
|
+
Accept:
|
|
363
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
|
364
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
365
|
+
Referer: "https://www.google.com/",
|
|
366
|
+
"Cache-Control": "no-cache",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
console.log("Navigating to URL...");
|
|
370
|
+
|
|
371
|
+
// Wrap entire operation with a global timeout
|
|
372
|
+
const content = await Promise.race([
|
|
373
|
+
(async () => {
|
|
374
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 120000 });
|
|
375
|
+
|
|
376
|
+
await page.waitForTimeout(3000); // DDoS challenge buffer (reduced from 10s)
|
|
377
|
+
|
|
378
|
+
const isApiRequest = url.includes("/api") || url.endsWith(".json");
|
|
379
|
+
|
|
380
|
+
if (!isApiRequest) {
|
|
381
|
+
try {
|
|
382
|
+
await page.waitForSelector(".episode-wrap, .episode-list", {
|
|
383
|
+
timeout: 60000,
|
|
384
|
+
});
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.log("Selector not found, continuing...");
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
try {
|
|
390
|
+
await page.waitForFunction(
|
|
391
|
+
() => {
|
|
392
|
+
const text = document.body.textContent;
|
|
393
|
+
return text.includes("{") && text.includes("}");
|
|
394
|
+
},
|
|
395
|
+
{ timeout: 60000 },
|
|
396
|
+
);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
console.log("API content not found, continuing...");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return await page.content();
|
|
403
|
+
})(),
|
|
404
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Playwright operation timed out after ${GLOBAL_TIMEOUT / 1000}s`)), GLOBAL_TIMEOUT)),
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
return content;
|
|
408
|
+
} finally {
|
|
409
|
+
await browser.close();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
static async fetchJson(url) {
|
|
414
|
+
if (flaresolverr.isEnabled()) {
|
|
415
|
+
const { body } = await flaresolverr.get(url);
|
|
416
|
+
return flaresolverr.extractJson(body);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const html = await this.fetch(url);
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Try to parse the content as JSON
|
|
423
|
+
const jsonMatch =
|
|
424
|
+
html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i) ||
|
|
425
|
+
html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
426
|
+
|
|
427
|
+
if (jsonMatch) {
|
|
428
|
+
try {
|
|
429
|
+
return JSON.parse(jsonMatch[1].trim());
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.log(
|
|
432
|
+
"Failed to parse JSON from matched content, trying whole page",
|
|
433
|
+
);
|
|
434
|
+
return JSON.parse(html);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
return JSON.parse(html);
|
|
438
|
+
}
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error("Failed to parse JSON:", error.message);
|
|
441
|
+
throw new Error(`Failed to parse JSON from ${url}: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
static async rawRequest(url, options = {}) {
|
|
446
|
+
try {
|
|
447
|
+
return await axios.get(url, options);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static async fetchCloudflareProtected(url, options = {}) {
|
|
454
|
+
console.log("Fetching Cloudflare-protected content from:", url);
|
|
455
|
+
|
|
456
|
+
if (flaresolverr.isEnabled()) {
|
|
457
|
+
const { body } = await flaresolverr.get(url);
|
|
458
|
+
return body;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const proxy = Config.proxyEnabled ? Config.getRandomProxy() : null;
|
|
462
|
+
console.log(`Using proxy: ${proxy || "none"}`);
|
|
463
|
+
|
|
464
|
+
const browser = await launchBrowser();
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const contextOptions = {
|
|
468
|
+
userAgent: Config.userAgent,
|
|
469
|
+
viewport: { width: 1920, height: 1080 },
|
|
470
|
+
extraHTTPHeaders: {
|
|
471
|
+
Accept:
|
|
472
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
|
473
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
474
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
475
|
+
"Cache-Control": "no-cache",
|
|
476
|
+
Pragma: "no-cache",
|
|
477
|
+
"Sec-Fetch-Dest": "document",
|
|
478
|
+
"Sec-Fetch-Mode": "navigate",
|
|
479
|
+
"Sec-Fetch-Site": "cross-site",
|
|
480
|
+
"Sec-Fetch-User": "?1",
|
|
481
|
+
"Upgrade-Insecure-Requests": "1",
|
|
482
|
+
Referer: options.referer || Config.getUrl("home"),
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
if (proxy) {
|
|
487
|
+
contextOptions.proxy = { server: proxy };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const context = await browser.newContext(contextOptions);
|
|
491
|
+
const page = await context.newPage();
|
|
492
|
+
|
|
493
|
+
await page.addInitScript(() => {
|
|
494
|
+
Object.defineProperty(navigator, "webdriver", { get: () => false });
|
|
495
|
+
Object.defineProperty(navigator, "plugins", {
|
|
496
|
+
get: () => [1, 2, 3, 4, 5],
|
|
497
|
+
});
|
|
498
|
+
Object.defineProperty(navigator, "languages", {
|
|
499
|
+
get: () => ["en-US", "en"],
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Add chrome object
|
|
503
|
+
window.chrome = {
|
|
504
|
+
runtime: {},
|
|
505
|
+
loadTimes: function () {},
|
|
506
|
+
csi: function () {},
|
|
507
|
+
app: {},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Mock permissions
|
|
511
|
+
const originalQuery = window.navigator.permissions.query;
|
|
512
|
+
window.navigator.permissions.query = (parameters) =>
|
|
513
|
+
parameters.name === "notifications"
|
|
514
|
+
? Promise.resolve({ state: Notification.permission })
|
|
515
|
+
: originalQuery(parameters);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
console.log("Navigating to URL...");
|
|
519
|
+
await page.goto(url, {
|
|
520
|
+
waitUntil: "domcontentloaded",
|
|
521
|
+
timeout: 60000,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Handle Cloudflare challenge
|
|
525
|
+
await this.handleCloudflareChallenge(page);
|
|
526
|
+
|
|
527
|
+
const content = await page.content();
|
|
528
|
+
await context.close();
|
|
529
|
+
|
|
530
|
+
return content;
|
|
531
|
+
} finally {
|
|
532
|
+
await browser.close();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
static async handleCloudflareChallenge(page) {
|
|
537
|
+
console.log("Checking for Cloudflare challenge...");
|
|
538
|
+
|
|
539
|
+
// for any immediate redirects
|
|
540
|
+
await page.waitForTimeout(3000);
|
|
541
|
+
|
|
542
|
+
// Check for various challenge indicators
|
|
543
|
+
const challengeSelectors = [
|
|
544
|
+
"#cf-challenge-running",
|
|
545
|
+
".cf-challenge-form",
|
|
546
|
+
"[data-ray]", // Cloudflare Ray ID
|
|
547
|
+
'title:has-text("Just a moment")',
|
|
548
|
+
'h1:has-text("Please wait")',
|
|
549
|
+
'div:has-text("Checking your browser")',
|
|
550
|
+
'div:has-text("DDoS protection")',
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
let challengeFound = false;
|
|
554
|
+
for (const selector of challengeSelectors) {
|
|
555
|
+
try {
|
|
556
|
+
const element = await page.$(selector);
|
|
557
|
+
if (element) {
|
|
558
|
+
challengeFound = true;
|
|
559
|
+
console.log(`Challenge detected with selector: ${selector}`);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
// Continue checking other selectors
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (challengeFound || page.url().includes("cdn-cgi/challenge")) {
|
|
568
|
+
console.log("Cloudflare challenge detected, waiting for resolution...");
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
await page.waitForFunction(
|
|
572
|
+
() => {
|
|
573
|
+
const title = document.title.toLowerCase();
|
|
574
|
+
const url = window.location.href;
|
|
575
|
+
const bodyText = document.body.textContent.toLowerCase();
|
|
576
|
+
|
|
577
|
+
// Check if we're no longer on a challenge page
|
|
578
|
+
return (
|
|
579
|
+
!title.includes("just a moment") &&
|
|
580
|
+
!title.includes("please wait") &&
|
|
581
|
+
!url.includes("cdn-cgi/challenge") &&
|
|
582
|
+
!bodyText.includes("checking your browser") &&
|
|
583
|
+
!bodyText.includes("ddos protection")
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
{ timeout: 30000 },
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
console.log("Cloudflare challenge resolved successfully");
|
|
590
|
+
|
|
591
|
+
// Additional wait to ensure page is fully loaded
|
|
592
|
+
await page.waitForTimeout(2000);
|
|
593
|
+
} catch (timeoutError) {
|
|
594
|
+
console.warn("Challenge resolution timeout - proceeding anyway");
|
|
595
|
+
// Don't throw error, let the caller handle the response
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
console.log("No Cloudflare challenge detected");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
static async fetchApiData(url, params = {}, cfClearance) {
|
|
603
|
+
cfClearance = cfClearance || Config.cfClearance || null;
|
|
604
|
+
|
|
605
|
+
const qs = new URLSearchParams(params).toString();
|
|
606
|
+
const fullUrl = qs ? url + '?' + qs : url;
|
|
607
|
+
|
|
608
|
+
if (cfClearance) {
|
|
609
|
+
try {
|
|
610
|
+
const { body, status } = await this._curlGet(fullUrl, {
|
|
611
|
+
cfClearance,
|
|
612
|
+
userAgent: Config.userAgent,
|
|
613
|
+
referer: Config.getUrl("home"),
|
|
614
|
+
timeout: 15000,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
if (
|
|
618
|
+
status === 403 ||
|
|
619
|
+
body.includes("DDoS-GUARD") ||
|
|
620
|
+
body.includes("checking your browser") ||
|
|
621
|
+
body.includes("Just a moment")
|
|
622
|
+
) {
|
|
623
|
+
console.log("[fetchApiData] cf_clearance failed (challenge page)");
|
|
624
|
+
} else {
|
|
625
|
+
return typeof body === "string" ? body : JSON.stringify(body);
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.log(`[fetchApiData] curl request: ${err.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (flaresolverr.isEnabled()) {
|
|
633
|
+
const target = new URL(url);
|
|
634
|
+
Object.keys(params).forEach((key) => {
|
|
635
|
+
target.searchParams.append(key, params[key]);
|
|
636
|
+
});
|
|
637
|
+
const result = await flaresolverr.get(target.toString());
|
|
638
|
+
this._saveFlareResult(result);
|
|
639
|
+
if (!url.includes("/api")) return result.body;
|
|
640
|
+
return flaresolverr.extractJson(result.body);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
throw new CustomError(
|
|
644
|
+
cfClearance
|
|
645
|
+
? "cf_clearance failed and FlareSolverr not enabled"
|
|
646
|
+
: "No cf_clearance available",
|
|
647
|
+
503,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Browser-based API fetch - uses Playwright instead of axios
|
|
653
|
+
* Used when cookies fail and we need to bypass DDoS-Guard
|
|
654
|
+
*/
|
|
655
|
+
static async fetchApiDataWithBrowser(url, params) {
|
|
656
|
+
console.log(`[fetchApiDataWithBrowser] Using Playwright for: ${url}`);
|
|
657
|
+
|
|
658
|
+
if (flaresolverr.isEnabled()) {
|
|
659
|
+
const target = new URL(url);
|
|
660
|
+
Object.keys(params).forEach((key) => {
|
|
661
|
+
target.searchParams.append(key, params[key]);
|
|
662
|
+
});
|
|
663
|
+
const { body } = await flaresolverr.get(target.toString());
|
|
664
|
+
return flaresolverr.extractJson(body);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const { launchBrowser } = require("./browser");
|
|
668
|
+
const browser = await launchBrowser();
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
const context = await browser.newContext({
|
|
672
|
+
userAgent: Config.userAgent,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const page = await context.newPage();
|
|
676
|
+
|
|
677
|
+
// Build full URL with params
|
|
678
|
+
const fullUrl = new URL(url);
|
|
679
|
+
Object.keys(params).forEach(key => {
|
|
680
|
+
fullUrl.searchParams.append(key, params[key]);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await page.goto(fullUrl.toString(), {
|
|
684
|
+
waitUntil: "domcontentloaded",
|
|
685
|
+
timeout: 60000,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Wait for JSON response
|
|
689
|
+
await page.waitForFunction(() => {
|
|
690
|
+
const text = document.body.textContent;
|
|
691
|
+
return text.trim().startsWith('{') || text.trim().startsWith('[');
|
|
692
|
+
}, { timeout: 15000 });
|
|
693
|
+
|
|
694
|
+
const content = await page.textContent();
|
|
695
|
+
const data = JSON.parse(content.trim());
|
|
696
|
+
|
|
697
|
+
await context.close();
|
|
698
|
+
return data;
|
|
699
|
+
|
|
700
|
+
} finally {
|
|
701
|
+
await browser.close();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
module.exports = RequestManager;
|