tt-help-cli-ycl 1.2.0 → 1.3.1

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/auto-core.mjs +174 -0
  3. package/src/cli/auto.js +94 -0
  4. package/src/cli/explore.js +117 -0
  5. package/src/cli/progress.js +111 -0
  6. package/src/cli/scrape.js +47 -0
  7. package/src/cli/utils.js +18 -0
  8. package/src/cli/videos.js +41 -0
  9. package/src/cli/watch.js +28 -0
  10. package/src/data-store.mjs +213 -0
  11. package/src/{explore-core.cjs → explore-core.mjs} +148 -157
  12. package/src/{get-user-videos-core.cjs → get-user-videos-core.mjs} +6 -23
  13. package/src/lib/args.js +19 -38
  14. package/src/lib/auto-browser.mjs +5 -12
  15. package/src/lib/browser/anti-detect.js +23 -0
  16. package/src/lib/browser/cdp.js +142 -0
  17. package/src/lib/browser/launch.js +43 -0
  18. package/src/lib/browser/page.js +62 -0
  19. package/src/lib/constants.js +13 -95
  20. package/src/lib/delay.js +54 -0
  21. package/src/lib/explore.js +16 -123
  22. package/src/lib/fetcher.js +3 -18
  23. package/src/lib/get-user-videos-browser.mjs +1 -6
  24. package/src/lib/io.js +8 -30
  25. package/src/lib/parser.js +1 -1
  26. package/src/lib/retry.js +44 -0
  27. package/src/lib/scrape-browser.mjs +1 -6
  28. package/src/lib/scrape.js +5 -4
  29. package/src/lib/url.js +52 -0
  30. package/src/main.mjs +59 -822
  31. package/src/scraper/{core.cjs → core.mjs} +25 -57
  32. package/src/scraper/modules/{comment-extractor.cjs → comment-extractor.mjs} +23 -15
  33. package/src/scraper/modules/follow-extractor.mjs +121 -0
  34. package/src/scraper/modules/{guess-extractor.cjs → guess-extractor.mjs} +3 -5
  35. package/src/scraper/modules/page-error-detector.mjs +68 -0
  36. package/src/scraper/modules/page-helpers.mjs +44 -0
  37. package/src/scraper/modules/scroll-collector.mjs +189 -0
  38. package/src/watch/public/index.html +139 -64
  39. package/src/watch/server.mjs +234 -153
  40. package/src/auto-core.cjs +0 -367
  41. package/src/data-store.cjs +0 -69
  42. package/src/get-user-videos.cjs +0 -59
  43. package/src/scraper/index.cjs +0 -97
  44. package/src/scraper/modules/follow-extractor.cjs +0 -112
  45. package/src/scraper/modules/page-helpers.cjs +0 -422
  46. package/src/scraper/modules/scroll-collector.cjs +0 -173
  47. package/src/scraper/modules/video-scanner.cjs +0 -43
@@ -0,0 +1,213 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ function inferStatus(u) {
5
+ if (u.restricted) return 'restricted';
6
+ if (u.error) return 'error';
7
+ if (u.processed) return 'done';
8
+ return 'pending';
9
+ }
10
+
11
+ export function createStore(filePath) {
12
+ let data = [];
13
+
14
+ if (filePath) {
15
+ const resolved = path.resolve(filePath);
16
+ if (fs.existsSync(resolved)) {
17
+ try {
18
+ const raw = fs.readFileSync(resolved, 'utf-8');
19
+ data = JSON.parse(raw);
20
+ if (!Array.isArray(data)) data = [];
21
+ } catch (e) {
22
+ console.error(`[data-store] 读取文件失败: ${e.message}`);
23
+ data = [];
24
+ }
25
+ }
26
+ }
27
+
28
+ for (const u of data) {
29
+ if (!u.status) u.status = inferStatus(u);
30
+ }
31
+
32
+ function save() {
33
+ if (!filePath) return;
34
+ const resolved = path.resolve(filePath);
35
+ try {
36
+ if (fs.existsSync(resolved)) {
37
+ const raw = fs.readFileSync(resolved, 'utf-8');
38
+ const diskData = JSON.parse(raw);
39
+ if (Array.isArray(diskData)) {
40
+ const memIds = new Set(data.map(u => u.uniqueId));
41
+ for (const diskUser of diskData) {
42
+ if (!memIds.has(diskUser.uniqueId)) {
43
+ if (!diskUser.status) diskUser.status = inferStatus(diskUser);
44
+ data.push(diskUser);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ } catch (e) { console.error(`[data-store] 合并磁盘数据失败: ${e.message}`); }
50
+ const json = JSON.stringify(data, null, 2);
51
+ fs.writeFileSync(resolved, json, 'utf-8');
52
+ }
53
+
54
+ function getUser(uid) {
55
+ return data.find(u => u.uniqueId === uid);
56
+ }
57
+
58
+ function hasUser(uid) {
59
+ return getUser(uid) !== undefined;
60
+ }
61
+
62
+ function addUser(user) {
63
+ const existing = getUser(user.uniqueId);
64
+ if (existing) {
65
+ for (const key of Object.keys(user)) {
66
+ if (key === 'uniqueId' || key === 'sources') continue;
67
+ if (user[key] !== undefined && user[key] !== null && user[key] !== '') {
68
+ existing[key] = user[key];
69
+ }
70
+ }
71
+ if (user.status && existing.status !== 'done') {
72
+ existing.status = user.status;
73
+ }
74
+ if (user.processed && !existing.processedAt) {
75
+ existing.processedAt = Date.now();
76
+ }
77
+ if (user.sources && Array.isArray(user.sources)) {
78
+ existing.sources = [...new Set([...(existing.sources || []), ...user.sources])];
79
+ }
80
+ } else {
81
+ if (!user.status) user.status = inferStatus(user);
82
+ if (user.processed) user.processedAt = user.processedAt || Date.now();
83
+ data.unshift(user);
84
+ }
85
+ }
86
+
87
+ function getPendingUsers() {
88
+ return data.filter(u => u.status === 'pending');
89
+ }
90
+
91
+ function getProcessedUsers() {
92
+ return data.filter(u => u.status === 'done');
93
+ }
94
+
95
+ function getAllUsers() {
96
+ return data;
97
+ }
98
+
99
+ function claimNextJob(expireMs = 5 * 60 * 1000) {
100
+ let next = data.find(u => u.status === 'pending');
101
+
102
+ if (!next) {
103
+ const now = Date.now();
104
+ const expired = data.find(u =>
105
+ u.status === 'processing' && u.claimedAt && (now - u.claimedAt) > expireMs
106
+ );
107
+ if (expired) {
108
+ expired.status = 'pending';
109
+ delete expired.claimedAt;
110
+ next = expired;
111
+ }
112
+ }
113
+
114
+ if (next) {
115
+ next.status = 'processing';
116
+ next.claimedAt = Date.now();
117
+ return { uniqueId: next.uniqueId, nickname: next.nickname };
118
+ }
119
+ return null;
120
+ }
121
+
122
+ function commitJob(uniqueId, result) {
123
+ const user = getUser(uniqueId);
124
+ if (!user) return { saved: false, error: 'user not found' };
125
+
126
+ if (result.restricted) {
127
+ user.status = 'restricted';
128
+ if (result.userInfo) {
129
+ const info = result.userInfo;
130
+ for (const key of Object.keys(info)) {
131
+ if (key === 'uniqueId' || key === 'sources') continue;
132
+ if (info[key] !== undefined && info[key] !== null && info[key] !== '') {
133
+ user[key] = info[key];
134
+ }
135
+ }
136
+ }
137
+ user.processed = true;
138
+ user.processedAt = Date.now();
139
+ user.noVideo = true;
140
+ user.sources = [...new Set([...(user.sources || []), 'restricted'])];
141
+ } else if (result.error) {
142
+ user.status = 'error';
143
+ user.error = result.error;
144
+ user.sources = [...new Set([...(user.sources || []), 'error'])];
145
+ } else {
146
+ user.status = 'done';
147
+ user.processed = true;
148
+ user.processedAt = Date.now();
149
+ user.followerCount = result.userInfo?.followerCount ?? user.followerCount;
150
+ user.videoCount = result.userInfo?.videoCount ?? user.videoCount;
151
+ user.nickname = result.userInfo?.nickname || user.nickname;
152
+ user.locationCreated = result.userInfo?.locationCreated || user.locationCreated;
153
+ user.ttSeller = result.userInfo?.ttSeller ?? user.ttSeller;
154
+ user.verified = result.userInfo?.verified ?? user.verified;
155
+ user.region = result.userInfo?.region || user.region;
156
+ user.signature = result.userInfo?.signature ?? user.signature;
157
+ user.followingCount = result.userInfo?.followingCount ?? user.followingCount;
158
+ user.heartCount = result.userInfo?.heartCount ?? user.heartCount;
159
+ if (result.userInfo?.secUid) user.secUid = result.userInfo.secUid;
160
+ const extraFields = ['restricted', 'error', 'userInfo', 'discoveredVideoAuthors',
161
+ 'discoveredCommentAuthors', 'discoveredGuessAuthors', 'discoveredFollowing',
162
+ 'discoveredFollowers', 'uniqueId', 'sources'];
163
+ for (const key of Object.keys(result)) {
164
+ if (extraFields.includes(key)) continue;
165
+ if (result[key] !== undefined && result[key] !== null && result[key] !== '') {
166
+ user[key] = result[key];
167
+ }
168
+ }
169
+ user.sources = [...new Set([...(user.sources || []), 'processed'])];
170
+
171
+ const discovered = [
172
+ ...(result.discoveredVideoAuthors || []).map(v => ({
173
+ uniqueId: v.uniqueId, nickname: v.nickname, locationCreated: v.locationCreated,
174
+ sources: ['video']
175
+ })),
176
+ ...(result.discoveredCommentAuthors || []).map(c => {
177
+ const id = typeof c === 'string' ? c.replace(/^@/, '') : c.uniqueId;
178
+ return typeof c === 'string'
179
+ ? { uniqueId: id, sources: ['comment'] }
180
+ : { uniqueId: id, ...c, sources: [...new Set([...(c.sources || []), 'comment'])] };
181
+ }),
182
+ ...(result.discoveredGuessAuthors || []).map(g => {
183
+ const id = typeof g === 'string' ? g.replace(/^@/, '') : g.uniqueId;
184
+ return typeof g === 'string'
185
+ ? { uniqueId: id, sources: ['guess'] }
186
+ : { uniqueId: id, ...g, sources: [...new Set([...(g.sources || []), 'guess'])] };
187
+ }),
188
+ ...(result.discoveredFollowing || []).map(([handle, name]) => ({
189
+ uniqueId: handle.replace(/^@/, ''), nickname: name, sources: ['following']
190
+ })),
191
+ ...(result.discoveredFollowers || []).map(([handle, name]) => ({
192
+ uniqueId: handle.replace(/^@/, ''), nickname: name, sources: ['follower']
193
+ })),
194
+ ];
195
+
196
+ for (const du of discovered) {
197
+ if (!du.uniqueId) continue;
198
+ addUser(du);
199
+ }
200
+ }
201
+
202
+ delete user.claimedAt;
203
+ save();
204
+ return { saved: true };
205
+ }
206
+
207
+ return {
208
+ save, getUser, hasUser, addUser,
209
+ getPendingUsers, getProcessedUsers, getAllUsers,
210
+ claimNextJob, commitJob,
211
+ data,
212
+ };
213
+ }
@@ -1,157 +1,148 @@
1
- const {
2
- delay,
3
- ensureBrowserReady,
4
- setDelayConfig,
5
- closeCommentPanel,
6
- retryWithBackoff,
7
- } = require('./scraper/modules/page-helpers.cjs');
8
- const {
9
- getUserInfo,
10
- collectVideos,
11
- isPageRestricted,
12
- } = require('./get-user-videos-core.cjs');
13
- const { scrapeSingleVideo } = require('./scraper/core.cjs');
14
- const { extractFollowAndFollowers } = require('./scraper/modules/follow-extractor.cjs');
15
- const { extractCommentAuthors } = require('./scraper/modules/comment-extractor.cjs');
16
- const { extractGuessVideos } = require('./scraper/modules/guess-extractor.cjs');
17
-
18
- async function processExplore(page, username, options, log) {
19
- const {
20
- maxComments = 0,
21
- maxGuess = 0,
22
- enableFollow = true,
23
- maxFollowing = 200,
24
- maxFollowers = 200,
25
- location = 'ES',
26
- } = options;
27
-
28
- const result = {
29
- userInfo: null,
30
- discoveredVideoAuthors: [],
31
- discoveredCommentAuthors: [],
32
- discoveredGuessAuthors: [],
33
- discoveredFollowing: [],
34
- discoveredFollowers: [],
35
- collectedVideos: 0,
36
- processed: false,
37
- hasFollowData: false,
38
- keepFollow: false,
39
- locationCreated: null,
40
- noVideo: false,
41
- error: null,
42
- };
43
-
44
- try {
45
- log(` 访问 @${username} 主页...`);
46
- const homeUrl = `https://www.tiktok.com/@${username}`;
47
- await retryWithBackoff(() => page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
48
- await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
49
- await delay(1000, 2000);
50
-
51
- // 1. 获取用户信息
52
- log(` 获取用户信息...`);
53
- const info = await getUserInfo(page);
54
- if (info) {
55
- result.userInfo = info;
56
- log(` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || '-'} | 视频: ${info.videoCount || '-'}`);
57
- }
58
-
59
- // 2. 获取关注+粉丝(在滚动前执行,避免按钮被滚出视口)
60
- if (enableFollow) {
61
- try {
62
- log(` 获取关注/粉丝...`);
63
- const { following, followers } = await extractFollowAndFollowers(
64
- page,
65
- { maxFollowing, maxFollowers, log }
66
- );
67
- result.discoveredFollowing = following || [];
68
- result.discoveredFollowers = followers || [];
69
- result.hasFollowData = true;
70
- log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
71
- } catch (e) {
72
- log(` 关注/粉丝提取失败: ${e.message}`);
73
- result.hasFollowData = false;
74
- result.discoveredFollowing = [];
75
- result.discoveredFollowers = [];
76
- }
77
- }
78
-
79
- // 3. 获取视频列表
80
- const videoList = await collectVideos(page, username, 1, log);
81
- const videoArray = videoList ? [...videoList.values()] : [];
82
- result.collectedVideos = videoArray.length;
83
-
84
- if (videoArray.length <= 0) {
85
- result.processed = true;
86
- result.noVideo = true;
87
- const restricted = await isPageRestricted(page);
88
- if (restricted) {
89
- result.restricted = true;
90
- log(` @${username} 页面受限(需登录),标记跳过`);
91
- } else {
92
- log(` @${username} 没有视频,标记已处理`);
93
- }
94
- return result;
95
- }
96
-
97
- // 4. 进入第一个视频
98
- const firstVideo = videoArray[0];
99
- const videoUrl = firstVideo.href.startsWith('http')
100
- ? firstVideo.href
101
- : `https://www.tiktok.com${firstVideo.href}`;
102
-
103
- log(` 进入第一个视频: ${videoUrl}`);
104
- await retryWithBackoff(() => page.goto(videoUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
105
- await delay(1500, 2500);
106
-
107
- // 5. 获取视频信息(含 locationCreated)
108
- const videoData = await scrapeSingleVideo(page, 0, 0, log, 'NEVER_MATCH');
109
- result.locationCreated = videoData.locationCreated || null;
110
- log(` 视频作者: ${videoData.videoAuthor} | 国家: ${result.locationCreated || '未知'}`);
111
-
112
- // 6. 判断是否为目标国家
113
- const isTargetLocation = result.locationCreated === location;
114
-
115
- if (isTargetLocation) {
116
- result.keepFollow = true;
117
- log(` 国家匹配 (${location}),获取评论和猜你喜欢...`);
118
-
119
- if (maxComments > 0) {
120
- const commentResult = await extractCommentAuthors(page, maxComments);
121
- result.discoveredCommentAuthors = commentResult || [];
122
- await closeCommentPanel(page);
123
- await delay(500, 1000);
124
- log(` 评论用户: ${result.discoveredCommentAuthors.length}`);
125
- }
126
-
127
- if (maxGuess > 0) {
128
- const guessResult = await extractGuessVideos(page, maxGuess);
129
- result.discoveredGuessAuthors = (guessResult || []).map(v => v.author).filter(Boolean);
130
- await closeCommentPanel(page);
131
- await delay(500, 1000);
132
- log(` 猜你喜欢作者: ${result.discoveredGuessAuthors.length}`);
133
- }
134
-
135
- result.discoveredVideoAuthors = [{
136
- uniqueId: videoData.uniqueId,
137
- nickname: videoData.nickname,
138
- locationCreated: videoData.locationCreated,
139
- }];
140
- } else {
141
- result.keepFollow = false;
142
- log(` 国家不匹配 (${result.locationCreated} !== ${location}),跳过评论/猜你喜欢,丢弃关注/粉丝`);
143
- result.discoveredFollowing = [];
144
- result.discoveredFollowers = [];
145
- }
146
-
147
- result.processed = true;
148
-
149
- } catch (e) {
150
- result.error = e.message;
151
- log(` [错误] ${e.message}`);
152
- }
153
-
154
- return result;
155
- }
156
-
157
- module.exports = { processExplore };
1
+ import {
2
+ delay,
3
+ setDelayConfig,
4
+ closeCommentPanel,
5
+ retryWithBackoff,
6
+ detectPageError,
7
+ } from './scraper/modules/page-helpers.mjs';
8
+ import {
9
+ getUserInfo,
10
+ collectVideos,
11
+ } from './get-user-videos-core.mjs';
12
+ import { scrapeSingleVideo } from './scraper/core.mjs';
13
+ import { extractFollowAndFollowers } from './scraper/modules/follow-extractor.mjs';
14
+ import { extractCommentAuthors } from './scraper/modules/comment-extractor.mjs';
15
+ import { extractGuessVideos } from './scraper/modules/guess-extractor.mjs';
16
+
17
+ async function processExplore(page, username, options, log) {
18
+ const {
19
+ maxComments = 0,
20
+ maxGuess = 0,
21
+ enableFollow = true,
22
+ maxFollowing = 5,
23
+ maxFollowers = 5,
24
+ location = 'ES',
25
+ } = options;
26
+
27
+ const result = {
28
+ userInfo: null,
29
+ discoveredVideoAuthors: [],
30
+ discoveredCommentAuthors: [],
31
+ discoveredGuessAuthors: [],
32
+ discoveredFollowing: [],
33
+ discoveredFollowers: [],
34
+ collectedVideos: 0,
35
+ processed: false,
36
+ hasFollowData: false,
37
+ keepFollow: false,
38
+ locationCreated: null,
39
+ noVideo: false,
40
+ error: null,
41
+ };
42
+
43
+ try {
44
+ log(` 访问 @${username} 主页...`);
45
+ const homeUrl = `https://www.tiktok.com/@${username}`;
46
+ await retryWithBackoff(() => page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
47
+ await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
48
+ await delay(1000, 2000);
49
+
50
+ log(' 获取用户信息...');
51
+ const info = await getUserInfo(page);
52
+ if (info) {
53
+ result.userInfo = info;
54
+ log(` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || '-'} | 视频: ${info.videoCount || '-'}`);
55
+ }
56
+
57
+ const videoList = await collectVideos(page, username, 1, log);
58
+ const videoArray = videoList ? [...videoList.values()] : [];
59
+ result.collectedVideos = videoArray.length;
60
+
61
+ if (videoArray.length <= 0) {
62
+ result.processed = true;
63
+ result.noVideo = true;
64
+ const pageError = await detectPageError(page);
65
+ if (pageError) {
66
+ result.restricted = true;
67
+ log(` @${username} 页面受限(${pageError}),标记跳过`);
68
+ } else {
69
+ log(` @${username} 没有视频,标记已处理`);
70
+ }
71
+ return result;
72
+ }
73
+
74
+ if (enableFollow) {
75
+ try {
76
+ log(' 获取关注/粉丝...');
77
+ const { following, followers } = await extractFollowAndFollowers(
78
+ page, { maxFollowing, maxFollowers, log }
79
+ );
80
+ result.discoveredFollowing = following || [];
81
+ result.discoveredFollowers = followers || [];
82
+ result.hasFollowData = true;
83
+ log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
84
+ } catch (e) {
85
+ log(` 关注/粉丝提取失败: ${e.message}`);
86
+ result.hasFollowData = false;
87
+ result.discoveredFollowing = [];
88
+ result.discoveredFollowers = [];
89
+ }
90
+ }
91
+
92
+ const firstVideo = videoArray[0];
93
+ const videoUrl = firstVideo.href.startsWith('http')
94
+ ? firstVideo.href
95
+ : `https://www.tiktok.com${firstVideo.href}`;
96
+
97
+ log(` 进入第一个视频: ${videoUrl}`);
98
+ await retryWithBackoff(() => page.goto(videoUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
99
+ await delay(1500, 2500);
100
+
101
+ const videoData = await scrapeSingleVideo(page, 0, 0, log, 'NEVER_MATCH');
102
+ result.locationCreated = videoData.locationCreated || null;
103
+ log(` 视频作者: ${videoData.videoAuthor} | 国家: ${result.locationCreated || '未知'}`);
104
+
105
+ const isTargetLocation = result.locationCreated === location;
106
+
107
+ if (isTargetLocation) {
108
+ result.keepFollow = true;
109
+ log(` 国家匹配 (${location}),获取评论和猜你喜欢...`);
110
+
111
+ if (maxComments > 0) {
112
+ const commentResult = await extractCommentAuthors(page, maxComments);
113
+ result.discoveredCommentAuthors = commentResult || [];
114
+ await closeCommentPanel(page);
115
+ await delay(500, 1000);
116
+ log(` 评论用户: ${result.discoveredCommentAuthors.length}`);
117
+ }
118
+
119
+ if (maxGuess > 0) {
120
+ const guessResult = await extractGuessVideos(page, maxGuess);
121
+ result.discoveredGuessAuthors = (guessResult || []).map(v => v.author).filter(Boolean);
122
+ await closeCommentPanel(page);
123
+ await delay(500, 1000);
124
+ log(` 猜你喜欢作者: ${result.discoveredGuessAuthors.length}`);
125
+ }
126
+
127
+ result.discoveredVideoAuthors = [{
128
+ uniqueId: videoData.uniqueId,
129
+ nickname: videoData.nickname,
130
+ locationCreated: videoData.locationCreated,
131
+ }];
132
+ } else {
133
+ result.keepFollow = false;
134
+ log(` 国家不匹配 (${result.locationCreated} !== ${location}),跳过评论/猜你喜欢,丢弃关注/粉丝`);
135
+ result.discoveredFollowing = [];
136
+ result.discoveredFollowers = [];
137
+ }
138
+
139
+ result.processed = true;
140
+ } catch (e) {
141
+ result.error = e.message;
142
+ log(` [错误] ${e.message}`);
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ export { processExplore };
@@ -1,12 +1,12 @@
1
- const { delay, ensureBrowserReady, ensureTikTokPage, retryWithBackoff } = require('./scraper/modules/page-helpers.cjs');
2
- const { scrollAndCollect } = require('./scraper/modules/scroll-collector.cjs');
1
+ import { delay, ensureBrowserReady, ensureTikTokPage, retryWithBackoff } from './scraper/modules/page-helpers.mjs';
2
+ import { scrollAndCollect } from './scraper/modules/scroll-collector.mjs';
3
3
 
4
4
  async function getUserInfo(page) {
5
5
  return await page.evaluate(() => {
6
6
  const html = document.documentElement.outerHTML;
7
7
  const result = {};
8
8
 
9
- const m = window.location.href.match(/\/@([^\/]+)/);
9
+ const m = window.location.href.match(/\/@([^/]+)/);
10
10
  if (m) result.uniqueId = m[1];
11
11
 
12
12
  const patterns = {
@@ -69,12 +69,9 @@ async function collectVideos(page, username, maxVideos, log) {
69
69
  },
70
70
  });
71
71
 
72
- // 去重
73
72
  const uniqueVideos = new Map();
74
73
  allLinks.forEach(v => {
75
- if (!uniqueVideos.has(v.id)) {
76
- uniqueVideos.set(v.id, v);
77
- }
74
+ if (!uniqueVideos.has(v.id)) uniqueVideos.set(v.id, v);
78
75
  });
79
76
 
80
77
  log(`收集完成: ${uniqueVideos.size} 个视频`);
@@ -82,12 +79,7 @@ async function collectVideos(page, username, maxVideos, log) {
82
79
  }
83
80
 
84
81
  async function runGetUserVideos(options) {
85
- const {
86
- username,
87
- maxVideos = 5,
88
- log = console.error,
89
- } = options;
90
-
82
+ const { username, maxVideos = 5, log = console.error } = options;
91
83
  const url = `https://www.tiktok.com/@${username}`;
92
84
 
93
85
  log(`用户: @${username}`);
@@ -131,13 +123,4 @@ async function runGetUserVideos(options) {
131
123
  return { output, browser };
132
124
  }
133
125
 
134
- async function isPageRestricted(page) {
135
- return await page.evaluate(() => {
136
- const bodyText = document.body.innerText;
137
- return !!(bodyText.includes('登录 TikTok') ||
138
- bodyText.includes('观众管理功能') ||
139
- bodyText.includes('Login to TikTok'));
140
- });
141
- }
142
-
143
- module.exports = { getUserInfo, collectVideos, runGetUserVideos, isPageRestricted };
126
+ export { getUserInfo, collectVideos, runGetUserVideos };