tt-help-cli-ycl 1.3.35 → 1.3.37

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.
@@ -1,11 +1,15 @@
1
- import { getOrCreatePage, isBrowserClosedError, relaunchBrowser } from '../lib/browser/page.js';
2
- import { delay, setDelayConfig } from '../scraper/modules/page-helpers.js';
3
- import { userId as configuredUserId, saveUserId } from '../lib/constants.js';
4
- import { getMacOrUuid } from '../lib/mac-or-uuid.js';
5
- import { ensureBrowserReady as ensureBrowserReadyCDP } from '../lib/browser/cdp.js';
6
- import { processRefresh } from '../scraper/refresh-core.js';
7
- import path from 'path';
8
- import os from 'os';
1
+ import {
2
+ getOrCreatePage,
3
+ isBrowserClosedError,
4
+ relaunchBrowser,
5
+ } from "../lib/browser/page.js";
6
+ import { delay, setDelayConfig } from "../scraper/modules/page-helpers.js";
7
+ import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
8
+ import { getMacOrUuid } from "../lib/mac-or-uuid.js";
9
+ import { ensureBrowserReady as ensureBrowserReadyCDP } from "../lib/browser/cdp.js";
10
+ import { processRefresh } from "../scraper/refresh-core.js";
11
+ import path from "path";
12
+ import os from "os";
9
13
 
10
14
  async function withRetry(fn, maxRetries = 5) {
11
15
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -13,12 +17,15 @@ async function withRetry(fn, maxRetries = 5) {
13
17
  return await fn();
14
18
  } catch (e) {
15
19
  if (attempt < maxRetries) {
16
- const waitTime = attempt <= 2
17
- ? 5000 + Math.random() * 5000
18
- : attempt <= 4
19
- ? 10000 + Math.random() * 10000
20
- : 20000 + Math.random() * 10000;
21
- console.error(` [网络] 请求失败 (${attempt}/${maxRetries}),${Math.round(waitTime / 1000)}s 后重试...`);
20
+ const waitTime =
21
+ attempt <= 2
22
+ ? 5000 + Math.random() * 5000
23
+ : attempt <= 4
24
+ ? 10000 + Math.random() * 10000
25
+ : 20000 + Math.random() * 10000;
26
+ console.error(
27
+ ` [网络] 请求失败 (${attempt}/${maxRetries}),${Math.round(waitTime / 1000)}s 后重试...`,
28
+ );
22
29
  await delay(waitTime / 1000, waitTime / 1000);
23
30
  } else {
24
31
  throw e;
@@ -35,8 +42,8 @@ async function apiGet(url) {
35
42
 
36
43
  async function apiPost(url, body) {
37
44
  const resp = await fetch(url, {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
40
47
  body: JSON.stringify(body),
41
48
  });
42
49
  if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
@@ -46,171 +53,236 @@ async function apiPost(url, body) {
46
53
  export async function handleRefresh(options) {
47
54
  const {
48
55
  explorePreset,
49
- explorePort, exploreProfile, exploreUserId, serverUrl,
56
+ explorePort,
57
+ exploreProfile,
58
+ exploreUserId,
59
+ serverUrl,
50
60
  } = options;
61
+ let browser = null;
62
+ let shuttingDown = false;
51
63
 
52
- let userId = exploreUserId || configuredUserId;
53
- if (!userId) {
54
- userId = await getMacOrUuid();
55
- saveUserId(userId);
56
- console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
57
- }
58
-
59
- setDelayConfig(explorePreset);
64
+ const shutdown = async (signal) => {
65
+ if (shuttingDown) return;
66
+ shuttingDown = true;
67
+ console.error(`\n[Refresh] 收到 ${signal},正在关闭浏览器...`);
68
+ await browser?.close().catch(() => {});
69
+ console.error("[Refresh] 已退出");
70
+ process.exit(0);
71
+ };
60
72
 
61
- console.error(`\n=== Refresh 模式 ===`);
62
- console.error(`服务器: ${serverUrl}`);
63
- console.error(`CDP 端口: ${explorePort || 9222}, 用户编号: ${userId}`);
64
- if (exploreProfile) console.error(`浏览器配置: ${exploreProfile}`);
65
- console.error(`刷新: 视频 100 + 关注 100 + 粉丝 100`);
66
- console.error(`新用户探索: 评论 + 猜你喜欢 + 关注/粉丝`);
73
+ const onSigint = () => {
74
+ void shutdown("SIGINT");
75
+ };
76
+ const onSigterm = () => {
77
+ void shutdown("SIGTERM");
78
+ };
67
79
 
68
- const cdpOptions = {};
69
- if (explorePort) cdpOptions.port = explorePort;
70
- if (exploreProfile) {
71
- cdpOptions.userDataDir = path.join(os.homedir(), 'Library', 'Application Support', `Microsoft Edge For Testing_${exploreProfile}`);
72
- }
80
+ process.once("SIGINT", onSigint);
81
+ process.once("SIGTERM", onSigterm);
73
82
 
74
- let browser = await ensureBrowserReadyCDP(cdpOptions);
75
- const page = await getOrCreatePage(browser);
83
+ try {
84
+ let userId = exploreUserId || configuredUserId;
85
+ if (!userId) {
86
+ userId = await getMacOrUuid();
87
+ saveUserId(userId);
88
+ console.error(`[初始化] 未检测到本地用户编号,已生成并使用: ${userId}`);
89
+ }
76
90
 
77
- let processedCount = 0;
78
- let errorCount = 0;
79
- let consecutiveNetworkErrors = 0;
91
+ setDelayConfig(explorePreset);
80
92
 
81
- console.error(`\n开始循环刷新任务...\n`);
93
+ console.error(`\n=== Refresh 模式 ===`);
94
+ console.error(`服务器: ${serverUrl}`);
95
+ console.error(`CDP 端口: ${explorePort || 9222}, 用户编号: ${userId}`);
96
+ if (exploreProfile) console.error(`浏览器配置: ${exploreProfile}`);
97
+ console.error(`刷新: 视频 100 + 关注 100 + 粉丝 100`);
98
+ console.error(`新用户探索: 评论 + 猜你喜欢 + 关注/粉丝`);
82
99
 
83
- while (true) {
84
- try {
85
- const jobData = await withRetry(() =>
86
- apiGet(`${serverUrl}/api/redo-job?userId=${userId}`)
100
+ const cdpOptions = {};
101
+ if (explorePort) cdpOptions.port = explorePort;
102
+ if (exploreProfile) {
103
+ cdpOptions.userDataDir = path.join(
104
+ os.homedir(),
105
+ "Library",
106
+ "Application Support",
107
+ `Microsoft Edge For Testing_${exploreProfile}`,
87
108
  );
109
+ }
88
110
 
89
- if (!jobData.hasJob) {
90
- console.error(`\n[空闲] 暂无 redo 任务,30s 后重试...`);
91
- await delay(30000, 30000);
92
- continue;
93
- }
111
+ browser = await ensureBrowserReadyCDP(cdpOptions);
112
+ const page = await getOrCreatePage(browser);
94
113
 
95
- const { uniqueId, nickname } = jobData.user;
96
- consecutiveNetworkErrors = 0;
97
- processedCount++;
114
+ let processedCount = 0;
115
+ let errorCount = 0;
116
+ let consecutiveNetworkErrors = 0;
98
117
 
99
- console.error(`\n[${processedCount}] 刷新 @${uniqueId} (${nickname || '未知'})...`);
118
+ console.error(`\n开始循环刷新任务...\n`);
100
119
 
101
- const result = await processRefresh(page, uniqueId, serverUrl, {
102
- maxFollowing: 100,
103
- maxFollowers: 100,
104
- maxVideos: 100,
105
- }, console.error);
120
+ while (!shuttingDown) {
121
+ try {
122
+ const jobData = await withRetry(() =>
123
+ apiGet(`${serverUrl}/api/redo-job?userId=${userId}`),
124
+ );
106
125
 
107
- if (result.restricted) {
108
- console.error(` @${uniqueId} 页面受限,跳过`);
109
- await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
110
- restricted: true,
111
- userInfo: result.userInfo || {},
112
- });
113
- continue;
114
- }
126
+ if (!jobData.hasJob) {
127
+ console.error(`\n[空闲] 暂无 redo 任务,30s 后重试...`);
128
+ await delay(30000, 30000);
129
+ continue;
130
+ }
131
+
132
+ const { uniqueId, nickname } = jobData.user;
133
+ consecutiveNetworkErrors = 0;
134
+ processedCount++;
135
+
136
+ console.error(
137
+ `\n[${processedCount}] 刷新 @${uniqueId} (${nickname || "未知"})...`,
138
+ );
115
139
 
116
- if (result.error) {
117
- // 浏览器关闭检测
118
- if (isBrowserClosedError(new Error(result.error))) {
119
- const newBrowser = await relaunchBrowser(cdpOptions, explorePort || 9222);
120
- browser = newBrowser;
121
- const newPage = await getOrCreatePage(browser);
122
- Object.assign(page, newPage);
123
- // 重试当前用户
124
- const retryResult = await processRefresh(page, uniqueId, serverUrl, {
140
+ const result = await processRefresh(
141
+ page,
142
+ uniqueId,
143
+ serverUrl,
144
+ {
125
145
  maxFollowing: 100,
126
146
  maxFollowers: 100,
127
147
  maxVideos: 100,
128
- }, console.error);
129
- Object.assign(result, retryResult);
130
- // 继续下方逻辑,检查重试后的 result
131
- } else {
132
- consecutiveNetworkErrors++;
133
- errorCount++;
134
- console.error(` [错误] ${result.error}`);
135
-
136
- if (consecutiveNetworkErrors >= 3) {
137
- console.error(` [警告] 连续 ${consecutiveNetworkErrors} 次错误,等待 60s 后重试...`);
138
- await delay(60000, 60000);
139
- consecutiveNetworkErrors = 0;
140
- }
148
+ },
149
+ console.error,
150
+ );
141
151
 
152
+ if (result.restricted) {
153
+ console.error(` @${uniqueId} 页面受限,跳过`);
142
154
  await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
143
- error: result.error,
155
+ restricted: true,
144
156
  userInfo: result.userInfo || {},
145
157
  });
146
- const errorType = consecutiveNetworkErrors > 1 ? 'network' : 'other';
147
- apiPost(`${serverUrl}/api/error-report`, {
158
+ continue;
159
+ }
160
+
161
+ if (result.error) {
162
+ // 浏览器关闭检测
163
+ if (isBrowserClosedError(new Error(result.error))) {
164
+ const newBrowser = await relaunchBrowser(
165
+ cdpOptions,
166
+ explorePort || 9222,
167
+ );
168
+ browser = newBrowser;
169
+ const newPage = await getOrCreatePage(browser);
170
+ Object.assign(page, newPage);
171
+ // 重试当前用户
172
+ const retryResult = await processRefresh(
173
+ page,
174
+ uniqueId,
175
+ serverUrl,
176
+ {
177
+ maxFollowing: 100,
178
+ maxFollowers: 100,
179
+ maxVideos: 100,
180
+ },
181
+ console.error,
182
+ );
183
+ Object.assign(result, retryResult);
184
+ // 继续下方逻辑,检查重试后的 result
185
+ } else {
186
+ consecutiveNetworkErrors++;
187
+ errorCount++;
188
+ console.error(` [错误] ${result.error}`);
189
+
190
+ if (consecutiveNetworkErrors >= 3) {
191
+ console.error(
192
+ ` [警告] 连续 ${consecutiveNetworkErrors} 次错误,等待 60s 后重试...`,
193
+ );
194
+ await delay(60000, 60000);
195
+ consecutiveNetworkErrors = 0;
196
+ }
197
+
198
+ await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
199
+ error: result.error,
200
+ userInfo: result.userInfo || {},
201
+ });
202
+ const errorType =
203
+ consecutiveNetworkErrors > 1 ? "network" : "other";
204
+ apiPost(`${serverUrl}/api/error-report`, {
205
+ userId,
206
+ username: uniqueId,
207
+ errorType,
208
+ errorMessage: result.error,
209
+ stage: "process",
210
+ errorStack: result.errorStack || "",
211
+ }).catch(() => {});
212
+ continue;
213
+ }
214
+ }
215
+
216
+ if (result.captchaDetected) {
217
+ await apiPost(`${serverUrl}/api/error-report`, {
148
218
  userId,
149
219
  username: uniqueId,
150
- errorType,
151
- errorMessage: result.error,
152
- stage: 'process',
153
- errorStack: result.errorStack || '',
154
- }).catch(() => {});
155
- continue;
220
+ errorType: "captcha",
221
+ errorMessage: result.captchaMessage || "页面出现验证码",
222
+ stage: result.captchaStage || "video-page",
223
+ errorStack: "",
224
+ });
156
225
  }
157
- }
158
226
 
159
- if (result.captchaDetected) {
160
- await apiPost(`${serverUrl}/api/error-report`, {
161
- userId,
162
- username: uniqueId,
163
- errorType: 'captcha',
164
- errorMessage: result.captchaMessage || '页面出现验证码',
165
- stage: result.captchaStage || 'video-page',
166
- errorStack: '',
227
+ consecutiveNetworkErrors = 0;
228
+
229
+ const guessedLocation = result.locationCreated || null;
230
+
231
+ await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
232
+ userInfo: result.userInfo || {},
233
+ discoveredVideoAuthors: (result.discoveredVideoAuthors || []).map(
234
+ (item) =>
235
+ typeof item === "object" ? { ...item, guessedLocation } : item,
236
+ ),
237
+ discoveredCommentAuthors: (result.discoveredCommentAuthors || []).map(
238
+ (author) => ({ author, guessedLocation }),
239
+ ),
240
+ discoveredGuessAuthors: (result.discoveredGuessAuthors || []).map(
241
+ (author) => ({ author, guessedLocation }),
242
+ ),
243
+ discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
244
+ handle: Array.isArray(f) ? f[0] : f,
245
+ displayName: Array.isArray(f) ? f[1] : null,
246
+ guessedLocation,
247
+ })),
248
+ discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
249
+ handle: Array.isArray(f) ? f[0] : f,
250
+ displayName: Array.isArray(f) ? f[1] : null,
251
+ guessedLocation,
252
+ })),
253
+ newUsersAdded: result.newUsersAdded || 0,
254
+ collectedVideos: result.collectedVideos || 0,
167
255
  });
168
- }
169
256
 
170
- consecutiveNetworkErrors = 0;
171
-
172
- const guessedLocation = result.locationCreated || null;
173
-
174
- await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
175
- userInfo: result.userInfo || {},
176
- discoveredVideoAuthors: (result.discoveredVideoAuthors || []).map(item =>
177
- typeof item === 'object' ? { ...item, guessedLocation } : item
178
- ),
179
- discoveredCommentAuthors: (result.discoveredCommentAuthors || []).map(author => ({ author, guessedLocation })),
180
- discoveredGuessAuthors: (result.discoveredGuessAuthors || []).map(author => ({ author, guessedLocation })),
181
- discoveredFollowing: (result.discoveredFollowing || []).map(f => ({
182
- handle: Array.isArray(f) ? f[0] : f,
183
- displayName: Array.isArray(f) ? f[1] : null,
184
- guessedLocation,
185
- })),
186
- discoveredFollowers: (result.discoveredFollowers || []).map(f => ({
187
- handle: Array.isArray(f) ? f[0] : f,
188
- displayName: Array.isArray(f) ? f[1] : null,
189
- guessedLocation,
190
- })),
191
- newUsersAdded: result.newUsersAdded || 0,
192
- collectedVideos: result.collectedVideos || 0,
193
- });
194
-
195
- console.error(` [完成] 视频: ${result.collectedVideos}, 评论作者: ${result.discoveredCommentAuthors?.length || 0}, 关注: ${result.discoveredFollowing?.length || 0}, 粉丝: ${result.discoveredFollowers?.length || 0}, 新增用户: ${result.newUsersAdded}`);
196
-
197
- await delay(3000, 5000);
198
- } catch (e) {
199
- consecutiveNetworkErrors++;
200
- errorCount++;
201
- console.error(`\n[错误] ${e.message}`);
257
+ console.error(
258
+ ` [完成] 视频: ${result.collectedVideos}, 评论作者: ${result.discoveredCommentAuthors?.length || 0}, 关注: ${result.discoveredFollowing?.length || 0}, 粉丝: ${result.discoveredFollowers?.length || 0}, 新增用户: ${result.newUsersAdded}`,
259
+ );
202
260
 
203
- if (consecutiveNetworkErrors >= 3) {
204
- console.error(`[警告] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 60s 后重试...`);
205
- await delay(60000, 60000);
206
- consecutiveNetworkErrors = 0;
207
- } else {
208
- const waitTime = consecutiveNetworkErrors <= 2
209
- ? 5000 + Math.random() * 5000
210
- : 10000 + Math.random() * 10000;
211
- console.error(` 等待 ${Math.round(waitTime / 1000)}s 后重试...`);
212
- await delay(waitTime / 1000, waitTime / 1000);
261
+ await delay(3000, 5000);
262
+ } catch (e) {
263
+ consecutiveNetworkErrors++;
264
+ errorCount++;
265
+ console.error(`\n[错误] ${e.message}`);
266
+
267
+ if (consecutiveNetworkErrors >= 3) {
268
+ console.error(
269
+ `[警告] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 60s 后重试...`,
270
+ );
271
+ await delay(60000, 60000);
272
+ consecutiveNetworkErrors = 0;
273
+ } else {
274
+ const waitTime =
275
+ consecutiveNetworkErrors <= 2
276
+ ? 5000 + Math.random() * 5000
277
+ : 10000 + Math.random() * 10000;
278
+ console.error(` 等待 ${Math.round(waitTime / 1000)}s 后重试...`);
279
+ await delay(waitTime / 1000, waitTime / 1000);
280
+ }
213
281
  }
214
282
  }
283
+ } finally {
284
+ process.removeListener("SIGINT", onSigint);
285
+ process.removeListener("SIGTERM", onSigterm);
286
+ await browser?.close().catch(() => {});
215
287
  }
216
288
  }
@@ -1,33 +1,144 @@
1
- import { readFileSync, writeFileSync } from 'fs';
2
- import { TikTokScraper } from '../lib/tiktok-scraper.mjs';
1
+ import path from "path";
2
+ import Database from "better-sqlite3";
3
+ import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
4
+
5
+ function resolveVideoSource(inputPath) {
6
+ if (!inputPath) return null;
7
+
8
+ const resolved = path.resolve(inputPath);
9
+ const ext = path.extname(resolved).toLowerCase();
10
+
11
+ if (ext === ".db") {
12
+ return { kind: "db", dbPath: resolved, label: resolved };
13
+ }
14
+
15
+ return null;
16
+ }
17
+
18
+ function ensureVideoStatsColumns(db) {
19
+ const existing = new Set(
20
+ db
21
+ .prepare("PRAGMA table_info(videos)")
22
+ .all()
23
+ .map((column) => column.name),
24
+ );
25
+ const required = {
26
+ play_count: "INTEGER",
27
+ digg_count: "INTEGER",
28
+ comment_count: "INTEGER",
29
+ share_count: "INTEGER",
30
+ collect_count: "INTEGER",
31
+ stats_updated_at: "INTEGER",
32
+ };
33
+ for (const [column, type] of Object.entries(required)) {
34
+ if (!existing.has(column)) {
35
+ db.exec(`ALTER TABLE videos ADD COLUMN ${column} ${type}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ function loadVideosFromDb(dbPath) {
41
+ const db = new Database(dbPath);
42
+ ensureVideoStatsColumns(db);
43
+ const rows = db
44
+ .prepare("SELECT * FROM videos ORDER BY registered_at ASC")
45
+ .all();
46
+ const videos = rows.map((row) => ({
47
+ id: row.id,
48
+ href: row.href,
49
+ authorUniqueId: row.author_unique_id,
50
+ locationCreated: row.location_created,
51
+ ttSeller: !!row.tt_seller,
52
+ registeredAt: row.registered_at,
53
+ userUpdateCount: row.user_update_count,
54
+ stats:
55
+ row.play_count === null &&
56
+ row.digg_count === null &&
57
+ row.comment_count === null &&
58
+ row.share_count === null &&
59
+ row.collect_count === null
60
+ ? undefined
61
+ : {
62
+ playCount: row.play_count,
63
+ diggCount: row.digg_count,
64
+ commentCount: row.comment_count,
65
+ shareCount: row.share_count,
66
+ collectCount: row.collect_count,
67
+ },
68
+ }));
69
+ return { db, videos };
70
+ }
71
+
72
+ function writeVideosToDb(db, videos) {
73
+ const stmt = db.prepare(`
74
+ UPDATE videos
75
+ SET play_count = ?,
76
+ digg_count = ?,
77
+ comment_count = ?,
78
+ share_count = ?,
79
+ collect_count = ?,
80
+ stats_updated_at = ?
81
+ WHERE id = ?
82
+ `);
83
+ const now = Date.now();
84
+ const txn = db.transaction((items) => {
85
+ for (const video of items) {
86
+ if (!video.stats) continue;
87
+ stmt.run(
88
+ video.stats.playCount ?? null,
89
+ video.stats.diggCount ?? null,
90
+ video.stats.commentCount ?? null,
91
+ video.stats.shareCount ?? null,
92
+ video.stats.collectCount ?? null,
93
+ now,
94
+ video.id,
95
+ );
96
+ }
97
+ });
98
+ txn(videos);
99
+ }
3
100
 
4
101
  export async function handleVideoStats(options) {
5
- const { statsFile, statsParallel } = options;
102
+ const statsSource = options.statsSource || options.statsFile;
103
+ const { statsParallel } = options;
6
104
 
7
- if (!statsFile) {
8
- console.error('用法: tt-help videostats <文件路径> -p <并发数>');
9
- console.error('示例: tt-help videostats data/result-videos.json -p 3');
10
- console.error('');
11
- console.error('选项: -p, --parallel <N> 并发数(默认: 3)');
105
+ if (!statsSource) {
106
+ console.error("用法: tt-help videostats <db路径> -p <并发数>");
107
+ console.error(" tt-help videostats data/result.db -p 3");
108
+ console.error("");
109
+ console.error("选项: -p, --parallel <N> 并发数(默认: 3)");
12
110
  process.exit(1);
13
111
  }
14
112
 
15
- // 读取文件
16
- const videos = JSON.parse(readFileSync(statsFile, 'utf-8'));
113
+ const source = resolveVideoSource(statsSource);
114
+ if (!source) {
115
+ console.error("[videostats] 仅支持 .db 路径");
116
+ process.exit(1);
117
+ }
118
+
119
+ let db = null;
120
+ let videos = [];
121
+ ({ db, videos } = loadVideosFromDb(source.dbPath));
122
+
17
123
  if (!Array.isArray(videos) || videos.length === 0) {
18
- console.error('[videostats] 文件为空或格式错误');
124
+ console.error("[videostats] 视频源为空或格式错误");
125
+ db?.close();
19
126
  process.exit(1);
20
127
  }
21
128
 
22
129
  const poolSize = Math.min(statsParallel, videos.length);
23
- console.error(`[videostats] 读取 ${statsFile}: ${videos.length} 个视频,并发: ${poolSize}`);
130
+ console.error(
131
+ `[videostats] 读取 ${source.label}: ${videos.length} 个视频,并发: ${poolSize}`,
132
+ );
24
133
 
25
134
  // 初始化 scraper
26
135
  const scraper = new TikTokScraper({ poolSize });
27
136
  await scraper.init();
28
- console.error('[videostats] 浏览器初始化完成,开始刷新...');
137
+ console.error("[videostats] 浏览器初始化完成,开始刷新...");
29
138
 
30
- let success = 0, failed = 0, skipped = 0;
139
+ let success = 0,
140
+ failed = 0,
141
+ skipped = 0;
31
142
 
32
143
  try {
33
144
  for (let i = 0; i < videos.length; i += poolSize) {
@@ -35,27 +146,29 @@ export async function handleVideoStats(options) {
35
146
 
36
147
  const results = await Promise.allSettled(
37
148
  batch.map(async (v) => {
38
- if (!v.href) return { video: v, error: 'no href' };
149
+ if (!v.href) return { video: v, error: "no href" };
39
150
  try {
40
151
  const info = await scraper.getVideoInfo(v.href);
41
- if (!info) return { video: v, error: 'no info' };
152
+ if (!info) return { video: v, error: "no info" };
42
153
  return { video: v, info };
43
154
  } catch (err) {
44
155
  return { video: v, error: err.message };
45
156
  }
46
- })
157
+ }),
47
158
  );
48
159
 
49
160
  for (const result of results) {
50
- if (result.status === 'fulfilled') {
161
+ if (result.status === "fulfilled") {
51
162
  const { video, info, error } = result.value;
52
163
  if (info && info.stats) {
53
164
  video.stats = info.stats;
54
165
  success++;
55
166
  } else {
56
167
  failed++;
57
- const id = video.id || video.href || '?';
58
- console.error(` [失败] ${id.substring(0, 30)} - ${error || '获取失败'}`);
168
+ const id = video.id || video.href || "?";
169
+ console.error(
170
+ ` [失败] ${id.substring(0, 30)} - ${error || "获取失败"}`,
171
+ );
59
172
  }
60
173
  } else {
61
174
  failed++;
@@ -63,19 +176,21 @@ export async function handleVideoStats(options) {
63
176
  }
64
177
 
65
178
  const processed = Math.min(i + poolSize, videos.length);
66
- console.error(`[videostats] 进度: ${processed}/${videos.length} (成功: ${success}, 失败: ${failed})`);
179
+ console.error(
180
+ `[videostats] 进度: ${processed}/${videos.length} (成功: ${success}, 失败: ${failed})`,
181
+ );
67
182
  }
68
183
 
69
- // 写回文件
70
- writeFileSync(statsFile, JSON.stringify(videos, null, 2), 'utf-8');
184
+ writeVideosToDb(db, videos);
71
185
  console.error(``);
72
186
  console.error(`[videostats] 完成: 成功 ${success}, 失败 ${failed}`);
73
- console.error(`[videostats] 已更新 ${statsFile}`);
74
-
187
+ console.error(`[videostats] 已更新 ${source.dbPath}`);
75
188
  } catch (err) {
76
189
  console.error(`[videostats] 异常: ${err.message}`);
190
+ db?.close();
77
191
  process.exit(1);
78
192
  } finally {
193
+ db?.close();
79
194
  await scraper.close();
80
195
  }
81
196
  }