tt-help-cli-ycl 1.3.45 → 1.3.47

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 (67) hide show
  1. package/README.md +33 -33
  2. package/cli.js +9 -9
  3. package/package.json +52 -52
  4. package/scripts/run-explore copy.bat +101 -101
  5. package/scripts/run-explore.bat +134 -134
  6. package/scripts/run-explore.ps1 +159 -159
  7. package/scripts/run-explore.sh +121 -121
  8. package/src/cli/attach.js +331 -313
  9. package/src/cli/auto.js +265 -265
  10. package/src/cli/comments.js +620 -620
  11. package/src/cli/config.js +170 -170
  12. package/src/cli/db-import.js +51 -51
  13. package/src/cli/explore.js +555 -555
  14. package/src/cli/info.js +10 -16
  15. package/src/cli/open.js +111 -111
  16. package/src/cli/progress.js +111 -111
  17. package/src/cli/refresh.js +288 -288
  18. package/src/cli/scrape.js +47 -47
  19. package/src/cli/utils.js +18 -18
  20. package/src/cli/videos.js +41 -41
  21. package/src/cli/videostats.js +196 -196
  22. package/src/cli/watch.js +30 -30
  23. package/src/cli/webserver.js +19 -0
  24. package/src/lib/api-interceptor.js +161 -161
  25. package/src/lib/args.js +809 -778
  26. package/src/lib/browser/anti-detect.js +23 -23
  27. package/src/lib/browser/cdp.js +261 -261
  28. package/src/lib/browser/health-checker.js +114 -114
  29. package/src/lib/browser/launch.js +43 -43
  30. package/src/lib/browser/page.js +184 -184
  31. package/src/lib/constants.js +297 -287
  32. package/src/lib/delay.js +54 -54
  33. package/src/lib/explore-fetch.js +118 -118
  34. package/src/lib/fetcher.js +45 -45
  35. package/src/lib/filter.js +66 -66
  36. package/src/lib/io.js +54 -54
  37. package/src/lib/output.js +80 -80
  38. package/src/lib/page-error-detector.js +109 -109
  39. package/src/lib/parse-ssr.mjs +69 -69
  40. package/src/lib/parser.js +47 -47
  41. package/src/lib/retry.js +45 -45
  42. package/src/lib/scrape.js +90 -89
  43. package/src/lib/target-locations.js +61 -61
  44. package/src/lib/tiktok-scraper.mjs +173 -105
  45. package/src/lib/url.js +52 -52
  46. package/src/main.js +73 -70
  47. package/src/npm-main.js +70 -69
  48. package/src/scraper/auto-core.js +203 -203
  49. package/src/scraper/core.js +255 -255
  50. package/src/scraper/explore-core.js +208 -208
  51. package/src/scraper/modules/captcha-handler.js +114 -114
  52. package/src/scraper/modules/follow-extractor.js +250 -250
  53. package/src/scraper/modules/guess-extractor.js +51 -51
  54. package/src/scraper/modules/page-helpers.js +48 -48
  55. package/src/scraper/refresh-core.js +213 -213
  56. package/src/videos/core.js +143 -143
  57. package/src/watch/data-store.js +2980 -2978
  58. package/src/watch/public/index.html +2355 -2345
  59. package/src/watch/server.js +727 -727
  60. package/src/webserver/server.mjs +174 -0
  61. package/scripts/test-captcha-lib.mjs +0 -68
  62. package/scripts/test-captcha.mjs +0 -81
  63. package/scripts/test-incognito-lib.mjs +0 -36
  64. package/scripts/test-login-state.mjs +0 -128
  65. package/scripts/test-safe-click.mjs +0 -45
  66. package/scripts/test-watch-db-smoke.mjs +0 -246
  67. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
@@ -1,250 +1,250 @@
1
- import { delay, getDelayConfig } from "./page-helpers.js";
2
- import { scrollAndCollect } from "./scroll-collector.js";
3
- import { extractUniqueId, toProfileUrl } from "../../lib/url.js";
4
-
5
- const FILTER_WORDS = ["主页", "已关注", "粉丝", "推荐"];
6
-
7
- const FOLLOW_TRIGGER_SELECTORS = [
8
- "[data-e2e=following]",
9
- 'a[href$="/following"]',
10
- 'a[href*="/following"]',
11
- '[data-e2e*="following"]',
12
- ];
13
-
14
- async function waitForFollowTrigger(page, timeout = 15000) {
15
- await page
16
- .waitForFunction(
17
- (selectors) => {
18
- for (const selector of selectors) {
19
- if (document.querySelector(selector)) return true;
20
- }
21
-
22
- const textMatchers = [/^关注$/, /^Following$/i, /^已关注$/];
23
- const nodes = document.querySelectorAll("a,button,div,span");
24
- for (const node of nodes) {
25
- const text = (node.textContent || "").trim();
26
- if (textMatchers.some((matcher) => matcher.test(text))) return true;
27
- }
28
-
29
- return false;
30
- },
31
- FOLLOW_TRIGGER_SELECTORS,
32
- { timeout },
33
- )
34
- .catch(() => {});
35
- }
36
-
37
- async function waitForListContent(page, minChildren = 1, timeout = 15000) {
38
- await page
39
- .waitForFunction(
40
- (min) => {
41
- const container = document.querySelector(
42
- "[class*=DivUserListContainer]",
43
- );
44
- return container && container.children.length >= min;
45
- },
46
- minChildren,
47
- { timeout },
48
- )
49
- .catch(() => {});
50
- }
51
-
52
- function getProfileUrlForRetry(page) {
53
- const currentUrl = page.url();
54
- const uniqueId = extractUniqueId(currentUrl);
55
- if (uniqueId) return toProfileUrl(uniqueId);
56
- return currentUrl;
57
- }
58
-
59
- async function openFollowModal(page, log = () => {}) {
60
- const tryOpen = async () =>
61
- page.evaluate((selectors) => {
62
- const clickTarget = (node) => {
63
- if (!node) return false;
64
- const clickable =
65
- node.closest('a,button,[role="button"]') ||
66
- node.parentElement ||
67
- node;
68
- clickable.click();
69
- return true;
70
- };
71
-
72
- for (const selector of selectors) {
73
- const node = document.querySelector(selector);
74
- if (clickTarget(node)) return selector;
75
- }
76
-
77
- const textMatchers = [/^关注$/, /^Following$/i, /^已关注$/];
78
- const nodes = Array.from(document.querySelectorAll("a,button,div,span"));
79
- for (const node of nodes) {
80
- const text = (node.textContent || "").trim();
81
- if (!text) continue;
82
- if (textMatchers.some((matcher) => matcher.test(text))) {
83
- if (clickTarget(node)) return `text:${text}`;
84
- }
85
- }
86
-
87
- return null;
88
- }, FOLLOW_TRIGGER_SELECTORS);
89
-
90
- let opened = null;
91
- for (let attempt = 1; attempt <= 3; attempt++) {
92
- await waitForFollowTrigger(page, attempt === 1 ? 15000 : 8000);
93
- opened = await tryOpen();
94
- if (opened) {
95
- if (attempt >= 2) {
96
- log(` [关注弹窗] 第 ${attempt} 次已点击关注入口: ${opened}`);
97
- }
98
- break;
99
- }
100
-
101
- if (attempt >= 2) {
102
- log(` [关注弹窗] 第 ${attempt} 次仍未找到可点击的关注入口`);
103
- }
104
-
105
- await page
106
- .evaluate(() => {
107
- window.scrollTo({ top: 0, behavior: "instant" });
108
- })
109
- .catch(() => {});
110
- await delay(800, 1500);
111
- }
112
-
113
- if (!opened) {
114
- log(" [关注弹窗] 重试后仍未找到关注入口");
115
- throw new Error(
116
- "未找到关注入口元素,请确认当前页面为用户主页或页面结构已变化",
117
- );
118
- }
119
-
120
- // 等待用户列表容器,超时说明可能被弹窗遮挡
121
- let containerReady = false;
122
- for (let attempt = 1; attempt <= 2; attempt++) {
123
- try {
124
- await page.waitForSelector("[class*=DivUserListContainer]", {
125
- timeout: 30000,
126
- });
127
- containerReady = true;
128
- if (attempt >= 2) {
129
- log(` [关注弹窗] 第 ${attempt} 次等待后,关注列表容器已出现`);
130
- }
131
- break;
132
- } catch (e) {
133
- if (attempt === 1) {
134
- // 第一次超时,重新 goto 用户主页后重试
135
- const retryUrl = getProfileUrlForRetry(page);
136
- log(
137
- ` [关注弹窗] 第 1 次等待列表超时,准备 goto 主页重试: ${retryUrl} | ${e.message}`,
138
- );
139
- await page.goto(retryUrl, { waitUntil: "domcontentloaded" });
140
- await delay(2000, 3000);
141
- log(" [关注弹窗] 主页跳转完成,重新点击关注入口");
142
- // 重新点击 follow 入口
143
- opened = await tryOpen();
144
- if (!opened) {
145
- log(" [关注弹窗] goto 主页后仍未找到关注入口");
146
- throw new Error(
147
- "goto 主页后仍未找到关注入口元素,请确认当前页面为用户主页",
148
- );
149
- }
150
- log(` [关注弹窗] goto 主页后已重新点击关注入口: ${opened}`);
151
- } else {
152
- log(` [关注弹窗] 第 2 次等待列表仍超时,结束本次提取: ${e.message}`);
153
- throw e;
154
- }
155
- }
156
- }
157
-
158
- if (containerReady) {
159
- await waitForListContent(page, 1, 5000);
160
- }
161
- }
162
-
163
- async function switchToFollowersTab(page) {
164
- await page.evaluate(() => {
165
- const tabs = document.querySelectorAll("[class*=DivTabItem]");
166
- for (const tab of tabs) {
167
- if (tab.textContent?.includes("粉丝")) {
168
- tab.click();
169
- return;
170
- }
171
- }
172
- throw new Error("未找到粉丝 Tab");
173
- });
174
- await page.waitForSelector("[class*=DivUserListContainer]", {
175
- timeout: 30000,
176
- });
177
- await waitForListContent(page, 1, 5000);
178
- }
179
-
180
- async function closeFollowModal(page) {
181
- await page.evaluate(() => {
182
- const closeBtn = document.querySelector("[data-e2e=follow-popup-close]");
183
- if (closeBtn) closeBtn.click();
184
- });
185
- await page.waitForTimeout(500);
186
- }
187
-
188
- function createUserCollectFn() {
189
- return (container) => {
190
- const FILTER_WORDS = ["主页", "已关注", "粉丝", "推荐"];
191
- const modal = document.querySelector("[class*=eyhy6180]");
192
- const root = modal || document;
193
- const users = [];
194
- const seen = new Set();
195
- const links = root.querySelectorAll('a[href*="/@"]');
196
- for (const link of links) {
197
- const match = link.href.match(/@([^/?]+)/);
198
- if (!match) continue;
199
- const handle = "@" + decodeURIComponent(match[1]);
200
- const text = (link.textContent || "").trim();
201
- if (text.length <= 2) continue;
202
- if (FILTER_WORDS.includes(text)) continue;
203
- if (seen.has(handle)) continue;
204
- seen.add(handle);
205
- users.push({ handle, displayName: text });
206
- }
207
- return { items: users };
208
- };
209
- }
210
-
211
- async function extractUsersFromModal(page, maxUsers) {
212
- const config = getDelayConfig();
213
- const minDelay = Math.max(300, Math.round(config.commentMax * 0.3));
214
- const maxDelay = Math.max(800, config.commentMax);
215
-
216
- const allUsers = await scrollAndCollect(page, {
217
- container: "[class*=DivUserListContainer]",
218
- findScrollable: false,
219
- collectFn: createUserCollectFn(),
220
- uniqueKey: (u) => u.handle,
221
- maxItems: maxUsers,
222
- delayRange: [minDelay, maxDelay],
223
- staleThreshold: 2,
224
- });
225
-
226
- return allUsers.slice(0, maxUsers);
227
- }
228
-
229
- async function extractFollowAndFollowers(page, options = {}) {
230
- const { maxFollowing = 999, maxFollowers = 999, log = () => {} } = options;
231
-
232
- await openFollowModal(page, log);
233
-
234
- const following = await extractUsersFromModal(page, maxFollowing);
235
- log(` 已关注: ${following.length}`);
236
-
237
- await switchToFollowersTab(page);
238
-
239
- const followers = await extractUsersFromModal(page, maxFollowers);
240
- log(` 粉丝: ${followers.length}`);
241
-
242
- await closeFollowModal(page);
243
-
244
- return {
245
- following: following.map((u) => [u.handle, u.displayName]),
246
- followers: followers.map((u) => [u.handle, u.displayName]),
247
- };
248
- }
249
-
250
- export { extractFollowAndFollowers };
1
+ import { delay, getDelayConfig } from "./page-helpers.js";
2
+ import { scrollAndCollect } from "./scroll-collector.js";
3
+ import { extractUniqueId, toProfileUrl } from "../../lib/url.js";
4
+
5
+ const FILTER_WORDS = ["主页", "已关注", "粉丝", "推荐"];
6
+
7
+ const FOLLOW_TRIGGER_SELECTORS = [
8
+ "[data-e2e=following]",
9
+ 'a[href$="/following"]',
10
+ 'a[href*="/following"]',
11
+ '[data-e2e*="following"]',
12
+ ];
13
+
14
+ async function waitForFollowTrigger(page, timeout = 15000) {
15
+ await page
16
+ .waitForFunction(
17
+ (selectors) => {
18
+ for (const selector of selectors) {
19
+ if (document.querySelector(selector)) return true;
20
+ }
21
+
22
+ const textMatchers = [/^关注$/, /^Following$/i, /^已关注$/];
23
+ const nodes = document.querySelectorAll("a,button,div,span");
24
+ for (const node of nodes) {
25
+ const text = (node.textContent || "").trim();
26
+ if (textMatchers.some((matcher) => matcher.test(text))) return true;
27
+ }
28
+
29
+ return false;
30
+ },
31
+ FOLLOW_TRIGGER_SELECTORS,
32
+ { timeout },
33
+ )
34
+ .catch(() => {});
35
+ }
36
+
37
+ async function waitForListContent(page, minChildren = 1, timeout = 15000) {
38
+ await page
39
+ .waitForFunction(
40
+ (min) => {
41
+ const container = document.querySelector(
42
+ "[class*=DivUserListContainer]",
43
+ );
44
+ return container && container.children.length >= min;
45
+ },
46
+ minChildren,
47
+ { timeout },
48
+ )
49
+ .catch(() => {});
50
+ }
51
+
52
+ function getProfileUrlForRetry(page) {
53
+ const currentUrl = page.url();
54
+ const uniqueId = extractUniqueId(currentUrl);
55
+ if (uniqueId) return toProfileUrl(uniqueId);
56
+ return currentUrl;
57
+ }
58
+
59
+ async function openFollowModal(page, log = () => {}) {
60
+ const tryOpen = async () =>
61
+ page.evaluate((selectors) => {
62
+ const clickTarget = (node) => {
63
+ if (!node) return false;
64
+ const clickable =
65
+ node.closest('a,button,[role="button"]') ||
66
+ node.parentElement ||
67
+ node;
68
+ clickable.click();
69
+ return true;
70
+ };
71
+
72
+ for (const selector of selectors) {
73
+ const node = document.querySelector(selector);
74
+ if (clickTarget(node)) return selector;
75
+ }
76
+
77
+ const textMatchers = [/^关注$/, /^Following$/i, /^已关注$/];
78
+ const nodes = Array.from(document.querySelectorAll("a,button,div,span"));
79
+ for (const node of nodes) {
80
+ const text = (node.textContent || "").trim();
81
+ if (!text) continue;
82
+ if (textMatchers.some((matcher) => matcher.test(text))) {
83
+ if (clickTarget(node)) return `text:${text}`;
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }, FOLLOW_TRIGGER_SELECTORS);
89
+
90
+ let opened = null;
91
+ for (let attempt = 1; attempt <= 3; attempt++) {
92
+ await waitForFollowTrigger(page, attempt === 1 ? 15000 : 8000);
93
+ opened = await tryOpen();
94
+ if (opened) {
95
+ if (attempt >= 2) {
96
+ log(` [关注弹窗] 第 ${attempt} 次已点击关注入口: ${opened}`);
97
+ }
98
+ break;
99
+ }
100
+
101
+ if (attempt >= 2) {
102
+ log(` [关注弹窗] 第 ${attempt} 次仍未找到可点击的关注入口`);
103
+ }
104
+
105
+ await page
106
+ .evaluate(() => {
107
+ window.scrollTo({ top: 0, behavior: "instant" });
108
+ })
109
+ .catch(() => {});
110
+ await delay(800, 1500);
111
+ }
112
+
113
+ if (!opened) {
114
+ log(" [关注弹窗] 重试后仍未找到关注入口");
115
+ throw new Error(
116
+ "未找到关注入口元素,请确认当前页面为用户主页或页面结构已变化",
117
+ );
118
+ }
119
+
120
+ // 等待用户列表容器,超时说明可能被弹窗遮挡
121
+ let containerReady = false;
122
+ for (let attempt = 1; attempt <= 2; attempt++) {
123
+ try {
124
+ await page.waitForSelector("[class*=DivUserListContainer]", {
125
+ timeout: 30000,
126
+ });
127
+ containerReady = true;
128
+ if (attempt >= 2) {
129
+ log(` [关注弹窗] 第 ${attempt} 次等待后,关注列表容器已出现`);
130
+ }
131
+ break;
132
+ } catch (e) {
133
+ if (attempt === 1) {
134
+ // 第一次超时,重新 goto 用户主页后重试
135
+ const retryUrl = getProfileUrlForRetry(page);
136
+ log(
137
+ ` [关注弹窗] 第 1 次等待列表超时,准备 goto 主页重试: ${retryUrl} | ${e.message}`,
138
+ );
139
+ await page.goto(retryUrl, { waitUntil: "domcontentloaded" });
140
+ await delay(2000, 3000);
141
+ log(" [关注弹窗] 主页跳转完成,重新点击关注入口");
142
+ // 重新点击 follow 入口
143
+ opened = await tryOpen();
144
+ if (!opened) {
145
+ log(" [关注弹窗] goto 主页后仍未找到关注入口");
146
+ throw new Error(
147
+ "goto 主页后仍未找到关注入口元素,请确认当前页面为用户主页",
148
+ );
149
+ }
150
+ log(` [关注弹窗] goto 主页后已重新点击关注入口: ${opened}`);
151
+ } else {
152
+ log(` [关注弹窗] 第 2 次等待列表仍超时,结束本次提取: ${e.message}`);
153
+ throw e;
154
+ }
155
+ }
156
+ }
157
+
158
+ if (containerReady) {
159
+ await waitForListContent(page, 1, 5000);
160
+ }
161
+ }
162
+
163
+ async function switchToFollowersTab(page) {
164
+ await page.evaluate(() => {
165
+ const tabs = document.querySelectorAll("[class*=DivTabItem]");
166
+ for (const tab of tabs) {
167
+ if (tab.textContent?.includes("粉丝")) {
168
+ tab.click();
169
+ return;
170
+ }
171
+ }
172
+ throw new Error("未找到粉丝 Tab");
173
+ });
174
+ await page.waitForSelector("[class*=DivUserListContainer]", {
175
+ timeout: 30000,
176
+ });
177
+ await waitForListContent(page, 1, 5000);
178
+ }
179
+
180
+ async function closeFollowModal(page) {
181
+ await page.evaluate(() => {
182
+ const closeBtn = document.querySelector("[data-e2e=follow-popup-close]");
183
+ if (closeBtn) closeBtn.click();
184
+ });
185
+ await page.waitForTimeout(500);
186
+ }
187
+
188
+ function createUserCollectFn() {
189
+ return (container) => {
190
+ const FILTER_WORDS = ["主页", "已关注", "粉丝", "推荐"];
191
+ const modal = document.querySelector("[class*=eyhy6180]");
192
+ const root = modal || document;
193
+ const users = [];
194
+ const seen = new Set();
195
+ const links = root.querySelectorAll('a[href*="/@"]');
196
+ for (const link of links) {
197
+ const match = link.href.match(/@([^/?]+)/);
198
+ if (!match) continue;
199
+ const handle = "@" + decodeURIComponent(match[1]);
200
+ const text = (link.textContent || "").trim();
201
+ if (text.length <= 2) continue;
202
+ if (FILTER_WORDS.includes(text)) continue;
203
+ if (seen.has(handle)) continue;
204
+ seen.add(handle);
205
+ users.push({ handle, displayName: text });
206
+ }
207
+ return { items: users };
208
+ };
209
+ }
210
+
211
+ async function extractUsersFromModal(page, maxUsers) {
212
+ const config = getDelayConfig();
213
+ const minDelay = Math.max(300, Math.round(config.commentMax * 0.3));
214
+ const maxDelay = Math.max(800, config.commentMax);
215
+
216
+ const allUsers = await scrollAndCollect(page, {
217
+ container: "[class*=DivUserListContainer]",
218
+ findScrollable: false,
219
+ collectFn: createUserCollectFn(),
220
+ uniqueKey: (u) => u.handle,
221
+ maxItems: maxUsers,
222
+ delayRange: [minDelay, maxDelay],
223
+ staleThreshold: 2,
224
+ });
225
+
226
+ return allUsers.slice(0, maxUsers);
227
+ }
228
+
229
+ async function extractFollowAndFollowers(page, options = {}) {
230
+ const { maxFollowing = 999, maxFollowers = 999, log = () => {} } = options;
231
+
232
+ await openFollowModal(page, log);
233
+
234
+ const following = await extractUsersFromModal(page, maxFollowing);
235
+ log(` 已关注: ${following.length}`);
236
+
237
+ await switchToFollowersTab(page);
238
+
239
+ const followers = await extractUsersFromModal(page, maxFollowers);
240
+ log(` 粉丝: ${followers.length}`);
241
+
242
+ await closeFollowModal(page);
243
+
244
+ return {
245
+ following: following.map((u) => [u.handle, u.displayName]),
246
+ followers: followers.map((u) => [u.handle, u.displayName]),
247
+ };
248
+ }
249
+
250
+ export { extractFollowAndFollowers };
@@ -1,51 +1,51 @@
1
- import { delay, getDelayConfig, closeCommentPanel } from './page-helpers.js';
2
- import { scrollAndCollect } from './scroll-collector.js';
3
-
4
- async function openGuessTab(page) {
5
- const tabs = page.locator('[class*="tabbar-item"]');
6
- const guessTab = tabs.filter({ hasText: /猜你喜欢/i }).first();
7
- await guessTab.click();
8
- const config = getDelayConfig();
9
- await delay(Math.round(config.commentMax * 0.5), config.commentMax);
10
- await page.waitForSelector('[class*="Related"]', { timeout: 5000 }).catch(() => {});
11
- }
12
-
13
- async function extractGuessVideos(page, maxVideos = 10) {
14
- await openGuessTab(page);
15
-
16
- const config = getDelayConfig();
17
- const allVideos = await scrollAndCollect(page, {
18
- container: '[class*="Related"]',
19
- findScrollable: true,
20
- collectFn: (container) => {
21
- const items = [];
22
- Array.from(container.querySelectorAll('[class*="DivItemContainer"]')).forEach(item => {
23
- const link = item.querySelector('a[href*="/video/"]');
24
- if (link) {
25
- const href = link.href || link.getAttribute('href');
26
- const m = href && href.match(/@([^/]+)\/video\/(\d+)/);
27
- if (m) {
28
- items.push({
29
- author: '@' + m[1],
30
- videoId: m[2],
31
- url: href,
32
- title: '',
33
- });
34
- }
35
- }
36
- });
37
- return { items };
38
- },
39
- uniqueKey: (v) => v.videoId,
40
- maxItems: maxVideos,
41
- delayRange: [Math.round(config.commentMax * 0.3), config.commentMax],
42
- staleThreshold: 3,
43
- });
44
-
45
- await closeCommentPanel(page);
46
- await delay(Math.round(config.commentMax * 0.3), config.commentMax);
47
-
48
- return allVideos.slice(0, maxVideos);
49
- }
50
-
51
- export { extractGuessVideos };
1
+ import { delay, getDelayConfig, closeCommentPanel } from './page-helpers.js';
2
+ import { scrollAndCollect } from './scroll-collector.js';
3
+
4
+ async function openGuessTab(page) {
5
+ const tabs = page.locator('[class*="tabbar-item"]');
6
+ const guessTab = tabs.filter({ hasText: /猜你喜欢/i }).first();
7
+ await guessTab.click();
8
+ const config = getDelayConfig();
9
+ await delay(Math.round(config.commentMax * 0.5), config.commentMax);
10
+ await page.waitForSelector('[class*="Related"]', { timeout: 5000 }).catch(() => {});
11
+ }
12
+
13
+ async function extractGuessVideos(page, maxVideos = 10) {
14
+ await openGuessTab(page);
15
+
16
+ const config = getDelayConfig();
17
+ const allVideos = await scrollAndCollect(page, {
18
+ container: '[class*="Related"]',
19
+ findScrollable: true,
20
+ collectFn: (container) => {
21
+ const items = [];
22
+ Array.from(container.querySelectorAll('[class*="DivItemContainer"]')).forEach(item => {
23
+ const link = item.querySelector('a[href*="/video/"]');
24
+ if (link) {
25
+ const href = link.href || link.getAttribute('href');
26
+ const m = href && href.match(/@([^/]+)\/video\/(\d+)/);
27
+ if (m) {
28
+ items.push({
29
+ author: '@' + m[1],
30
+ videoId: m[2],
31
+ url: href,
32
+ title: '',
33
+ });
34
+ }
35
+ }
36
+ });
37
+ return { items };
38
+ },
39
+ uniqueKey: (v) => v.videoId,
40
+ maxItems: maxVideos,
41
+ delayRange: [Math.round(config.commentMax * 0.3), config.commentMax],
42
+ staleThreshold: 3,
43
+ });
44
+
45
+ await closeCommentPanel(page);
46
+ await delay(Math.round(config.commentMax * 0.3), config.commentMax);
47
+
48
+ return allVideos.slice(0, maxVideos);
49
+ }
50
+
51
+ export { extractGuessVideos };