tt-help-cli-ycl 1.3.12 → 1.3.13
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/README.md +17 -17
- package/cli.js +9 -9
- package/package.json +45 -45
- package/scripts/run-explore.bat +68 -68
- package/scripts/run-explore.ps1 +81 -81
- package/scripts/run-explore.sh +73 -73
- package/scripts/test-captcha-lib.mjs +68 -0
- package/scripts/test-captcha.mjs +81 -0
- package/scripts/test-incognito-lib.mjs +36 -0
- package/scripts/test-login-state.mjs +128 -0
- package/scripts/test-safe-click.mjs +45 -0
- package/src/cli/auto.js +186 -157
- package/src/cli/explore.js +227 -193
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +216 -0
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/watch.js +31 -31
- package/src/lib/args.js +456 -402
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +52 -10
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +146 -87
- package/src/lib/constants.js +119 -115
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +40 -40
- package/src/lib/url.js +52 -52
- package/src/main.js +2 -0
- package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
- package/src/scraper/auto-core.js +203 -194
- package/src/scraper/core.js +211 -190
- package/src/scraper/explore-core.js +180 -171
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/comment-extractor.js +74 -69
- package/src/scraper/modules/follow-extractor.js +121 -121
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +179 -0
- package/src/videos/core.js +126 -126
- package/src/watch/data-store.js +431 -302
- package/src/watch/public/index.html +721 -701
- package/src/watch/server.js +483 -359
package/src/watch/data-store.js
CHANGED
|
@@ -1,302 +1,431 @@
|
|
|
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
|
-
let clientErrors = new Map();
|
|
14
|
-
|
|
15
|
-
let backupTimer = null;
|
|
16
|
-
|
|
17
|
-
if (filePath) {
|
|
18
|
-
const resolved = path.resolve(filePath);
|
|
19
|
-
const backupDir = path.join(path.dirname(resolved), '.backup');
|
|
20
|
-
const maxBackups = 3;
|
|
21
|
-
|
|
22
|
-
if (fs.existsSync(resolved)) {
|
|
23
|
-
try {
|
|
24
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
25
|
-
data = JSON.parse(content);
|
|
26
|
-
if (!Array.isArray(data)) {
|
|
27
|
-
data = [];
|
|
28
|
-
}
|
|
29
|
-
} catch (e) {
|
|
30
|
-
console.error(`[data-store] 读取文件失败: ${e.message}`);
|
|
31
|
-
data = [];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runBackup() {
|
|
36
|
-
if (!fs.existsSync(resolved)) return;
|
|
37
|
-
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
38
|
-
const now = new Date();
|
|
39
|
-
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 13);
|
|
40
|
-
const backupFile = path.join(backupDir, `data-${timestamp}.json`);
|
|
41
|
-
try {
|
|
42
|
-
fs.copyFileSync(resolved, backupFile);
|
|
43
|
-
const files = fs.readdirSync(backupDir)
|
|
44
|
-
.filter(f => f.startsWith('data-') && f.endsWith('.json'))
|
|
45
|
-
.sort()
|
|
46
|
-
.map(f => path.join(backupDir, f));
|
|
47
|
-
while (files.length > maxBackups) {
|
|
48
|
-
fs.unlinkSync(files.shift());
|
|
49
|
-
}
|
|
50
|
-
} catch (e) {
|
|
51
|
-
console.error(`[data-store] 备份失败: ${e.message}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
backupTimer = setInterval(runBackup, 60 * 60 * 1000);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const u of data) {
|
|
59
|
-
if (!u.status) u.status = inferStatus(u);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function save() {
|
|
63
|
-
if (!filePath) return;
|
|
64
|
-
const resolved = path.resolve(filePath);
|
|
65
|
-
const json = JSON.stringify(data, null, 2);
|
|
66
|
-
fs.writeFileSync(resolved, json, 'utf-8');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function stopBackup() {
|
|
70
|
-
if (backupTimer) {
|
|
71
|
-
clearInterval(backupTimer);
|
|
72
|
-
backupTimer = null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getUser(uid) {
|
|
77
|
-
return data.find(u => u.uniqueId === uid);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function hasUser(uid) {
|
|
81
|
-
return getUser(uid) !== undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function
|
|
106
|
-
return data.filter(u => u.status === '
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function
|
|
110
|
-
return data;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
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
|
+
let clientErrors = new Map();
|
|
14
|
+
|
|
15
|
+
let backupTimer = null;
|
|
16
|
+
|
|
17
|
+
if (filePath) {
|
|
18
|
+
const resolved = path.resolve(filePath);
|
|
19
|
+
const backupDir = path.join(path.dirname(resolved), '.backup');
|
|
20
|
+
const maxBackups = 3;
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(resolved)) {
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
25
|
+
data = JSON.parse(content);
|
|
26
|
+
if (!Array.isArray(data)) {
|
|
27
|
+
data = [];
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(`[data-store] 读取文件失败: ${e.message}`);
|
|
31
|
+
data = [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runBackup() {
|
|
36
|
+
if (!fs.existsSync(resolved)) return;
|
|
37
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 13);
|
|
40
|
+
const backupFile = path.join(backupDir, `data-${timestamp}.json`);
|
|
41
|
+
try {
|
|
42
|
+
fs.copyFileSync(resolved, backupFile);
|
|
43
|
+
const files = fs.readdirSync(backupDir)
|
|
44
|
+
.filter(f => f.startsWith('data-') && f.endsWith('.json'))
|
|
45
|
+
.sort()
|
|
46
|
+
.map(f => path.join(backupDir, f));
|
|
47
|
+
while (files.length > maxBackups) {
|
|
48
|
+
fs.unlinkSync(files.shift());
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(`[data-store] 备份失败: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
backupTimer = setInterval(runBackup, 60 * 60 * 1000);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const u of data) {
|
|
59
|
+
if (!u.status) u.status = inferStatus(u);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function save() {
|
|
63
|
+
if (!filePath) return;
|
|
64
|
+
const resolved = path.resolve(filePath);
|
|
65
|
+
const json = JSON.stringify(data, null, 2);
|
|
66
|
+
fs.writeFileSync(resolved, json, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stopBackup() {
|
|
70
|
+
if (backupTimer) {
|
|
71
|
+
clearInterval(backupTimer);
|
|
72
|
+
backupTimer = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getUser(uid) {
|
|
77
|
+
return data.find(u => u.uniqueId === uid);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasUser(uid) {
|
|
81
|
+
return getUser(uid) !== undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function userExists(uid) {
|
|
85
|
+
return hasUser(uid);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addUser(user, append) {
|
|
89
|
+
const existing = getUser(user.uniqueId);
|
|
90
|
+
if (existing) {
|
|
91
|
+
for (const key of Object.keys(user)) {
|
|
92
|
+
if (key === 'uniqueId' || key === 'sources') continue;
|
|
93
|
+
if (user[key] !== undefined && user[key] !== null && user[key] !== '') {
|
|
94
|
+
existing[key] = user[key];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
if (!user.status) user.status = inferStatus(user);
|
|
99
|
+
if (user.processed) user.processedAt = user.processedAt || Date.now();
|
|
100
|
+
if (append) data.push(user);
|
|
101
|
+
else data.unshift(user);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getPendingUsers() {
|
|
106
|
+
return data.filter(u => u.status === 'pending');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getProcessedUsers() {
|
|
110
|
+
return data.filter(u => u.status === 'done');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getAllUsers() {
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function claimNextJob(userId, expireMs = 5 * 60 * 1000) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
|
|
120
|
+
// 0. 该客户端有未过期的任务,续期返回
|
|
121
|
+
const ongoing = data.find(u =>
|
|
122
|
+
u.status === 'processing' && u.claimedBy === userId && u.claimedAt && (now - u.claimedAt) < expireMs
|
|
123
|
+
);
|
|
124
|
+
if (ongoing) {
|
|
125
|
+
ongoing.claimedAt = now;
|
|
126
|
+
return { uniqueId: ongoing.uniqueId, nickname: ongoing.nickname, claimedAt: ongoing.claimedAt, claimedBy: userId };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 按猜测国家梯队排序:第一梯队优先,无国家排最后
|
|
130
|
+
const tier1 = new Set(['PL', 'NL', 'BE']);
|
|
131
|
+
const tier2 = new Set(['DE', 'FR', 'IT', 'IE', 'ES']);
|
|
132
|
+
function locationTier(u) {
|
|
133
|
+
const loc = (u.guessedLocation || '').toUpperCase();
|
|
134
|
+
if (tier1.has(loc)) return 0;
|
|
135
|
+
if (tier2.has(loc)) return 1;
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pickFirst(sorted) {
|
|
140
|
+
return sorted.length > 0 ? sorted[0] : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let next = data.find(u => u.status === 'pending' && u.pinned);
|
|
144
|
+
|
|
145
|
+
if (!next) {
|
|
146
|
+
const expired = data.find(u =>
|
|
147
|
+
u.status === 'processing' && u.claimedAt && (now - u.claimedAt) > expireMs
|
|
148
|
+
);
|
|
149
|
+
if (expired) {
|
|
150
|
+
expired.status = 'pending';
|
|
151
|
+
delete expired.claimedAt;
|
|
152
|
+
next = expired;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!next) {
|
|
157
|
+
const seed = data.filter(u => u.status === 'pending' && u.sources && u.sources.includes('seed'));
|
|
158
|
+
seed.sort((a, b) => locationTier(a) - locationTier(b));
|
|
159
|
+
next = pickFirst(seed);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!next) {
|
|
163
|
+
const follow = data.filter(u => u.status === 'pending' && u.sources && (u.sources.includes('following') || u.sources.includes('follower')));
|
|
164
|
+
follow.sort((a, b) => locationTier(a) - locationTier(b));
|
|
165
|
+
next = pickFirst(follow);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!next) {
|
|
169
|
+
const all = data.filter(u => u.status === 'pending');
|
|
170
|
+
all.sort((a, b) => locationTier(a) - locationTier(b));
|
|
171
|
+
next = pickFirst(all);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (next) {
|
|
175
|
+
next.status = 'processing';
|
|
176
|
+
next.claimedAt = now;
|
|
177
|
+
next.claimedBy = userId;
|
|
178
|
+
return { uniqueId: next.uniqueId, nickname: next.nickname, claimedAt: next.claimedAt, claimedBy: userId };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function processDiscoveredUsers(result) {
|
|
184
|
+
const guessedLocation = result.guessedLocation || null;
|
|
185
|
+
const discovered = [
|
|
186
|
+
...(result.discoveredVideoAuthors || []).map(v => ({
|
|
187
|
+
uniqueId: typeof v === 'string' ? v.replace(/^@/, '') : v.uniqueId?.replace(/^@/, '') || '',
|
|
188
|
+
nickname: typeof v === 'string' ? null : v.nickname || null,
|
|
189
|
+
locationCreated: typeof v === 'string' ? null : v.locationCreated || null,
|
|
190
|
+
guessedLocation: typeof v === 'string' ? guessedLocation : (v.guessedLocation || guessedLocation),
|
|
191
|
+
source: 'video',
|
|
192
|
+
})),
|
|
193
|
+
...(result.discoveredCommentAuthors || []).map(c => {
|
|
194
|
+
if (typeof c === 'string') return { uniqueId: c.replace(/^@/, ''), source: 'comment', guessedLocation };
|
|
195
|
+
return { uniqueId: (c.author || c.uniqueId || '').replace(/^@/, ''), nickname: c.nickname || null, source: 'comment', guessedLocation: c.guessedLocation || guessedLocation };
|
|
196
|
+
}),
|
|
197
|
+
...(result.discoveredGuessAuthors || []).map(g => {
|
|
198
|
+
if (typeof g === 'string') return { uniqueId: g.replace(/^@/, ''), source: 'guess', guessedLocation };
|
|
199
|
+
return { uniqueId: (g.author || g.uniqueId || '').replace(/^@/, ''), nickname: g.nickname || null, source: 'guess', guessedLocation: g.guessedLocation || guessedLocation };
|
|
200
|
+
}),
|
|
201
|
+
...(result.discoveredFollowing || []).map(f => {
|
|
202
|
+
const handle = Array.isArray(f) ? f[0] : (f.handle || '');
|
|
203
|
+
const name = Array.isArray(f) ? f[1] : (f.displayName || null);
|
|
204
|
+
return { uniqueId: handle.replace(/^@/, ''), nickname: name, source: 'following', guessedLocation: (typeof f === 'object' && f.guessedLocation) || guessedLocation };
|
|
205
|
+
}),
|
|
206
|
+
...(result.discoveredFollowers || []).map(f => {
|
|
207
|
+
const handle = Array.isArray(f) ? f[0] : (f.handle || '');
|
|
208
|
+
const name = Array.isArray(f) ? f[1] : (f.displayName || null);
|
|
209
|
+
return { uniqueId: handle.replace(/^@/, ''), nickname: name, source: 'follower', guessedLocation: (typeof f === 'object' && f.guessedLocation) || guessedLocation };
|
|
210
|
+
}),
|
|
211
|
+
].filter(u => u.uniqueId);
|
|
212
|
+
|
|
213
|
+
const newUsers = [];
|
|
214
|
+
for (const d of discovered) {
|
|
215
|
+
const existing = getUser(d.uniqueId);
|
|
216
|
+
if (!existing) {
|
|
217
|
+
addUser(d, true);
|
|
218
|
+
newUsers.push(d.uniqueId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return newUsers;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function updateUserFromResult(user, result) {
|
|
225
|
+
if (result.restricted) {
|
|
226
|
+
user.status = 'restricted';
|
|
227
|
+
if (result.userInfo) {
|
|
228
|
+
const info = result.userInfo;
|
|
229
|
+
for (const key of Object.keys(info)) {
|
|
230
|
+
if (key === 'uniqueId' || key === 'sources') continue;
|
|
231
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== '') {
|
|
232
|
+
user[key] = info[key];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
user.processed = true;
|
|
237
|
+
user.processedAt = Date.now();
|
|
238
|
+
user.sources = [...new Set([...(user.sources || []), 'restricted'])];
|
|
239
|
+
} else if (result.error) {
|
|
240
|
+
user.status = 'error';
|
|
241
|
+
user.error = result.error;
|
|
242
|
+
user.sources = [...new Set([...(user.sources || []), 'error'])];
|
|
243
|
+
} else {
|
|
244
|
+
user.status = 'done';
|
|
245
|
+
user.processed = true;
|
|
246
|
+
user.processedAt = Date.now();
|
|
247
|
+
user.noVideo = result.noVideo || false;
|
|
248
|
+
user.keepFollow = result.keepFollow || false;
|
|
249
|
+
user.hasFollowData = result.hasFollowData || false;
|
|
250
|
+
|
|
251
|
+
if (result.userInfo) {
|
|
252
|
+
const info = result.userInfo;
|
|
253
|
+
for (const key of Object.keys(info)) {
|
|
254
|
+
if (key === 'uniqueId' || key === 'sources') continue;
|
|
255
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== '') {
|
|
256
|
+
user[key] = info[key];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
user.followerCount = result.userInfo?.followerCount ?? user.followerCount;
|
|
262
|
+
user.videoCount = result.userInfo?.videoCount ?? user.videoCount;
|
|
263
|
+
user.nickname = result.userInfo?.nickname || user.nickname;
|
|
264
|
+
user.locationCreated = result.userInfo?.locationCreated || user.locationCreated;
|
|
265
|
+
user.ttSeller = result.userInfo?.ttSeller ?? user.ttSeller;
|
|
266
|
+
user.verified = result.userInfo?.verified ?? user.verified;
|
|
267
|
+
user.region = result.userInfo?.region || user.region;
|
|
268
|
+
user.signature = result.userInfo?.signature ?? user.signature;
|
|
269
|
+
user.followingCount = result.userInfo?.followingCount ?? user.followingCount;
|
|
270
|
+
user.heartCount = result.userInfo?.heartCount ?? user.heartCount;
|
|
271
|
+
if (result.userInfo?.secUid) user.secUid = result.userInfo.secUid;
|
|
272
|
+
const extraFields = ['restricted', 'error', 'userInfo', 'discoveredVideoAuthors',
|
|
273
|
+
'discoveredCommentAuthors', 'discoveredGuessAuthors', 'discoveredFollowing',
|
|
274
|
+
'discoveredFollowers', 'uniqueId', 'sources'];
|
|
275
|
+
for (const key of Object.keys(result)) {
|
|
276
|
+
if (extraFields.includes(key)) continue;
|
|
277
|
+
if (result[key] !== undefined && result[key] !== null && result[key] !== '') {
|
|
278
|
+
user[key] = result[key];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function commitJob(uniqueId, result) {
|
|
285
|
+
const user = getUser(uniqueId);
|
|
286
|
+
if (!user) return { saved: false, error: 'user not found' };
|
|
287
|
+
|
|
288
|
+
updateUserFromResult(user, result);
|
|
289
|
+
delete user.claimedAt;
|
|
290
|
+
const newUsers = processDiscoveredUsers(result);
|
|
291
|
+
|
|
292
|
+
save();
|
|
293
|
+
return { saved: true, status: user.status, newUsers };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function commitNewExplore(uniqueId, result) {
|
|
297
|
+
const existing = getUser(uniqueId);
|
|
298
|
+
if (existing) {
|
|
299
|
+
updateUserFromResult(existing, result);
|
|
300
|
+
const newUsers = processDiscoveredUsers(result);
|
|
301
|
+
save();
|
|
302
|
+
return { saved: true, created: false, status: existing.status, newUsers };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const userObj = {
|
|
306
|
+
uniqueId,
|
|
307
|
+
...(result.userInfo || {}),
|
|
308
|
+
sources: ['refresh-explore'],
|
|
309
|
+
};
|
|
310
|
+
updateUserFromResult(userObj, result);
|
|
311
|
+
addUser(userObj, true);
|
|
312
|
+
const newUsers = processDiscoveredUsers(result);
|
|
313
|
+
|
|
314
|
+
save();
|
|
315
|
+
return { saved: true, created: true, status: userObj.status, newUsers };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resetJob(uniqueId) {
|
|
319
|
+
const user = getUser(uniqueId);
|
|
320
|
+
if (!user) return { saved: false, error: 'user not found' };
|
|
321
|
+
user.status = 'pending';
|
|
322
|
+
delete user.claimedAt;
|
|
323
|
+
delete user.processedAt;
|
|
324
|
+
delete user.processed;
|
|
325
|
+
delete user.error;
|
|
326
|
+
delete user.restricted;
|
|
327
|
+
delete user.noVideo;
|
|
328
|
+
save();
|
|
329
|
+
return { saved: true };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function togglePin(uniqueId) {
|
|
333
|
+
const user = getUser(uniqueId);
|
|
334
|
+
if (!user) return { saved: false, error: 'user not found' };
|
|
335
|
+
user.pinned = !user.pinned;
|
|
336
|
+
save();
|
|
337
|
+
return { saved: true, pinned: user.pinned };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getNextRedoJob(userId) {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const defaultTime = new Date('2016-01-01T00:00:00Z').getTime();
|
|
343
|
+
|
|
344
|
+
// 筛选目标国家用户,按 refreshTime 升序取最远的(没有则默认 2016-01-01)
|
|
345
|
+
const targetLocations = ['ES', 'PL', 'NL', 'BE', 'DE', 'FR', 'IT', 'IE'];
|
|
346
|
+
const targetUsers = data.filter(u => u.ttSeller && u.verified === false && targetLocations.includes(u.locationCreated));
|
|
347
|
+
if (targetUsers.length === 0) return null;
|
|
348
|
+
|
|
349
|
+
targetUsers.sort((a, b) => {
|
|
350
|
+
const ta = a.refreshTime || defaultTime;
|
|
351
|
+
const tb = b.refreshTime || defaultTime;
|
|
352
|
+
return ta - tb;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const next = targetUsers[0];
|
|
356
|
+
next.refreshTime = now;
|
|
357
|
+
return { uniqueId: next.uniqueId, nickname: next.nickname, refreshTime: next.refreshTime };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function commitRedoJob(uniqueId, result) {
|
|
361
|
+
const user = getUser(uniqueId);
|
|
362
|
+
if (!user) return { saved: false, error: 'user not found' };
|
|
363
|
+
|
|
364
|
+
user.refreshTime = Date.now();
|
|
365
|
+
|
|
366
|
+
if (result.userInfo) {
|
|
367
|
+
const info = result.userInfo;
|
|
368
|
+
for (const key of Object.keys(info)) {
|
|
369
|
+
if (key === 'uniqueId' || key === 'sources') continue;
|
|
370
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== '') {
|
|
371
|
+
user[key] = info[key];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { saved: true };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function reportClientError(userId, errorType, errorMessage, username, stage, errorStack) {
|
|
380
|
+
const existing = clientErrors.get(userId);
|
|
381
|
+
if (existing) {
|
|
382
|
+
existing.timestamp = Date.now();
|
|
383
|
+
if (errorType === 'captcha') {
|
|
384
|
+
existing.captchaCount = (existing.captchaCount || 0) + 1;
|
|
385
|
+
if (!existing.captchaStage) existing.captchaStage = stage || '';
|
|
386
|
+
if (!existing.captchaMessage) existing.captchaMessage = errorMessage || '';
|
|
387
|
+
if (!existing.captchaStack) existing.captchaStack = errorStack || '';
|
|
388
|
+
} else {
|
|
389
|
+
existing.errorType = errorType;
|
|
390
|
+
existing.errorMessage = errorMessage || '';
|
|
391
|
+
existing.errorStack = errorStack || '';
|
|
392
|
+
existing.stage = stage || '';
|
|
393
|
+
existing.reportCount = (existing.reportCount || 1) + 1;
|
|
394
|
+
}
|
|
395
|
+
if (username) existing.username = username;
|
|
396
|
+
} else {
|
|
397
|
+
clientErrors.set(userId, {
|
|
398
|
+
userId,
|
|
399
|
+
errorType,
|
|
400
|
+
errorMessage: errorMessage || '',
|
|
401
|
+
errorStack: errorStack || '',
|
|
402
|
+
username,
|
|
403
|
+
stage: stage || '',
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
reportCount: 1,
|
|
406
|
+
captchaCount: errorType === 'captcha' ? 1 : 0,
|
|
407
|
+
captchaStage: errorType === 'captcha' ? (stage || '') : '',
|
|
408
|
+
captchaMessage: errorType === 'captcha' ? (errorMessage || '') : '',
|
|
409
|
+
captchaStack: errorType === 'captcha' ? (errorStack || '') : '',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function deleteClientError(userId) {
|
|
415
|
+
clientErrors.delete(userId);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getClientErrors() {
|
|
419
|
+
return Array.from(clientErrors.values());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
save, getUser, hasUser, userExists, addUser,
|
|
424
|
+
getPendingUsers, getProcessedUsers, getAllUsers,
|
|
425
|
+
claimNextJob, commitJob, commitNewExplore, resetJob, togglePin,
|
|
426
|
+
getNextRedoJob, commitRedoJob,
|
|
427
|
+
reportClientError, deleteClientError, getClientErrors,
|
|
428
|
+
stopBackup,
|
|
429
|
+
data,
|
|
430
|
+
};
|
|
431
|
+
}
|