tt-help-cli-ycl 1.3.45 → 1.3.46
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 +33 -33
- package/cli.js +9 -9
- package/package.json +52 -52
- package/scripts/run-explore copy.bat +101 -101
- package/scripts/run-explore.bat +134 -134
- package/scripts/run-explore.ps1 +159 -159
- package/scripts/run-explore.sh +121 -121
- package/src/cli/attach.js +331 -313
- package/src/cli/auto.js +265 -265
- package/src/cli/comments.js +620 -620
- package/src/cli/config.js +170 -170
- package/src/cli/db-import.js +51 -51
- package/src/cli/explore.js +555 -555
- package/src/cli/info.js +10 -16
- package/src/cli/open.js +111 -111
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +288 -288
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/videostats.js +196 -196
- package/src/cli/watch.js +30 -30
- package/src/cli/webserver.js +19 -0
- package/src/lib/api-interceptor.js +161 -161
- package/src/lib/args.js +809 -778
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +261 -261
- package/src/lib/browser/health-checker.js +114 -114
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +184 -184
- package/src/lib/constants.js +297 -287
- 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/page-error-detector.js +109 -109
- package/src/lib/parse-ssr.mjs +69 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +90 -89
- package/src/lib/target-locations.js +61 -61
- package/src/lib/tiktok-scraper.mjs +160 -106
- package/src/lib/url.js +52 -52
- package/src/main.js +73 -70
- package/src/npm-main.js +70 -69
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +255 -255
- package/src/scraper/explore-core.js +208 -208
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/follow-extractor.js +250 -250
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +213 -213
- package/src/videos/core.js +143 -143
- package/src/watch/data-store.js +2980 -2978
- package/src/watch/public/index.html +2355 -2345
- package/src/watch/server.js +727 -727
- package/src/webserver/server.mjs +174 -0
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- package/scripts/test-watch-db-smoke.mjs +0 -246
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import { chromium } from "playwright";
|
|
2
4
|
import { detectBrowser } from "./browser/launch.js";
|
|
3
5
|
import { parseUserInfo, parseVideoInfo } from "./parse-ssr.mjs";
|
|
@@ -7,11 +9,22 @@ const DEFAULT_WAF_TTL = 120000;
|
|
|
7
9
|
const DEFAULT_WARM_URL = "https://www.tiktok.com/@nike";
|
|
8
10
|
const BROWSER_CLOSE_TIMEOUT = 5000;
|
|
9
11
|
const DEFAULT_MAX_REQUESTS_PER_PAGE = 50;
|
|
12
|
+
const FALLBACK_PROFILE_PORT = 9222;
|
|
10
13
|
|
|
11
14
|
function delay(ms) {
|
|
12
15
|
return new Promise((r) => setTimeout(r, ms));
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
function getFallbackProfileDir() {
|
|
19
|
+
const profile = `p${FALLBACK_PROFILE_PORT}`;
|
|
20
|
+
return path.join(
|
|
21
|
+
os.homedir(),
|
|
22
|
+
"Library",
|
|
23
|
+
"Application Support",
|
|
24
|
+
`Microsoft Edge For Testing_${profile}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
class PageSlot {
|
|
16
29
|
constructor(page) {
|
|
17
30
|
this.page = page;
|
|
@@ -47,6 +60,46 @@ class PromiseQueue {
|
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
function createLaunchOptions(executablePath) {
|
|
64
|
+
return {
|
|
65
|
+
headless: true,
|
|
66
|
+
executablePath,
|
|
67
|
+
handleSIGINT: false,
|
|
68
|
+
handleSIGTERM: false,
|
|
69
|
+
handleSIGHUP: false,
|
|
70
|
+
args: [
|
|
71
|
+
"--no-sandbox",
|
|
72
|
+
"--disable-setuid-sandbox",
|
|
73
|
+
"--disable-dev-shm-usage",
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function initContext(executablePath, poolSize, userDataDir) {
|
|
79
|
+
let context;
|
|
80
|
+
let browser = null;
|
|
81
|
+
const slots = [];
|
|
82
|
+
|
|
83
|
+
if (userDataDir) {
|
|
84
|
+
context = await chromium.launchPersistentContext(userDataDir, createLaunchOptions(executablePath));
|
|
85
|
+
const existing = context.pages();
|
|
86
|
+
if (existing.length > 0) {
|
|
87
|
+
slots.push(new PageSlot(existing[0]));
|
|
88
|
+
}
|
|
89
|
+
for (let i = slots.length; i < poolSize; i++) {
|
|
90
|
+
slots.push(new PageSlot(await context.newPage()));
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
browser = await chromium.launch(createLaunchOptions(executablePath));
|
|
94
|
+
context = await browser.newContext();
|
|
95
|
+
for (let i = 0; i < poolSize; i++) {
|
|
96
|
+
slots.push(new PageSlot(await context.newPage()));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { browser, context, slots };
|
|
101
|
+
}
|
|
102
|
+
|
|
50
103
|
export class TikTokScraper {
|
|
51
104
|
constructor({
|
|
52
105
|
poolSize = DEFAULT_POOL_SIZE,
|
|
@@ -64,6 +117,10 @@ export class TikTokScraper {
|
|
|
64
117
|
this.slotIdx = 0;
|
|
65
118
|
this.lastWarmTime = 0;
|
|
66
119
|
this.warmPromise = null;
|
|
120
|
+
this._fallbackBrowser = null;
|
|
121
|
+
this._fallbackContext = null;
|
|
122
|
+
this._fallbackSlots = [];
|
|
123
|
+
this._fallbackSlotIdx = 0;
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
async init() {
|
|
@@ -73,49 +130,56 @@ export class TikTokScraper {
|
|
|
73
130
|
"未找到本地浏览器(Chrome/Edge),请先安装浏览器或执行 npx playwright install",
|
|
74
131
|
);
|
|
75
132
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
handleSIGTERM: false,
|
|
81
|
-
handleSIGHUP: false,
|
|
82
|
-
args: [
|
|
83
|
-
"--no-sandbox",
|
|
84
|
-
"--disable-setuid-sandbox",
|
|
85
|
-
"--disable-dev-shm-usage",
|
|
86
|
-
],
|
|
87
|
-
});
|
|
88
|
-
this.context = await this.browser.newContext();
|
|
89
|
-
for (let i = 0; i < this.poolSize; i++) {
|
|
90
|
-
this.slots.push(new PageSlot(await this.context.newPage()));
|
|
91
|
-
}
|
|
133
|
+
const { browser, context, slots } = await initContext(executablePath, this.poolSize, null);
|
|
134
|
+
this.browser = browser;
|
|
135
|
+
this.context = context;
|
|
136
|
+
this.slots = slots;
|
|
92
137
|
await this.warmWaf();
|
|
93
138
|
}
|
|
94
139
|
|
|
140
|
+
async _ensureFallback(executablePath) {
|
|
141
|
+
if (this._fallbackContext) return;
|
|
142
|
+
const fallbackDir = getFallbackProfileDir();
|
|
143
|
+
console.error(`[TikTokScraper] 无登录态获取失败,fallback 到登录态 profile: ${fallbackDir}`);
|
|
144
|
+
const { browser, context, slots } = await initContext(executablePath, 1, fallbackDir);
|
|
145
|
+
this._fallbackBrowser = browser;
|
|
146
|
+
this._fallbackContext = context;
|
|
147
|
+
this._fallbackSlots = slots;
|
|
148
|
+
}
|
|
149
|
+
|
|
95
150
|
async close() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
151
|
+
const closeAll = async (browser) => {
|
|
152
|
+
if (browser) {
|
|
153
|
+
let closeTimedOut = false;
|
|
154
|
+
const closePromise = browser.close().catch((error) => {
|
|
155
|
+
console.error(
|
|
156
|
+
`[TikTokScraper] browser.close() failed: ${error.message}`,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
await Promise.race([
|
|
160
|
+
closePromise,
|
|
161
|
+
delay(BROWSER_CLOSE_TIMEOUT).then(() => { closeTimedOut = true; }),
|
|
162
|
+
]);
|
|
163
|
+
if (closeTimedOut) {
|
|
164
|
+
console.error(
|
|
165
|
+
`[TikTokScraper] browser.close() 超时 ${BROWSER_CLOSE_TIMEOUT}ms,跳过等待并继续退出`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
114
168
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// 无登录态的 browser 是 chromium.launch() 返回的,直接 close
|
|
172
|
+
await closeAll(this.browser);
|
|
173
|
+
// fallback 的 context 是 launchPersistentContext 返回的,直接 close
|
|
174
|
+
if (this._fallbackContext) {
|
|
175
|
+
try { await this._fallbackContext.close(); } catch {}
|
|
118
176
|
}
|
|
177
|
+
this.browser = null;
|
|
178
|
+
this.context = null;
|
|
179
|
+
this.slots = [];
|
|
180
|
+
this._fallbackBrowser = null;
|
|
181
|
+
this._fallbackContext = null;
|
|
182
|
+
this._fallbackSlots = [];
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
async restart() {
|
|
@@ -125,7 +189,7 @@ export class TikTokScraper {
|
|
|
125
189
|
|
|
126
190
|
isAlive() {
|
|
127
191
|
try {
|
|
128
|
-
return !!(this.
|
|
192
|
+
return !!(this.context && (!this.browser || this.browser.isConnected()));
|
|
129
193
|
} catch {
|
|
130
194
|
return false;
|
|
131
195
|
}
|
|
@@ -152,98 +216,88 @@ export class TikTokScraper {
|
|
|
152
216
|
return this.warmPromise;
|
|
153
217
|
}
|
|
154
218
|
|
|
155
|
-
|
|
219
|
+
isWarmExpired() {
|
|
156
220
|
return Date.now() - this.lastWarmTime > this.wafTtl;
|
|
157
221
|
}
|
|
158
222
|
|
|
159
|
-
_pickSlot() {
|
|
160
|
-
const slot =
|
|
161
|
-
this.slotIdx++;
|
|
223
|
+
_pickSlot(slots, idx, setIdx) {
|
|
224
|
+
const slot = slots[idx % slots.length];
|
|
162
225
|
return slot;
|
|
163
226
|
}
|
|
164
227
|
|
|
165
|
-
async _ensurePage(slot) {
|
|
228
|
+
async _ensurePage(slot, context) {
|
|
166
229
|
try {
|
|
167
|
-
if (
|
|
168
|
-
!slot.page.isClosed() &&
|
|
169
|
-
slot.requestCount < this.maxRequestsPerPage
|
|
170
|
-
) {
|
|
230
|
+
if (!slot.page.isClosed() && slot.requestCount < this.maxRequestsPerPage) {
|
|
171
231
|
return slot.page;
|
|
172
232
|
}
|
|
173
233
|
} catch {}
|
|
174
234
|
await slot.page?.close().catch(() => {});
|
|
175
|
-
slot.page = await
|
|
235
|
+
slot.page = await context.newPage();
|
|
176
236
|
slot.requestCount = 0;
|
|
177
237
|
return slot.page;
|
|
178
238
|
}
|
|
179
239
|
|
|
180
|
-
async _fetchViewSource(url,
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
waitUntil: "domcontentloaded",
|
|
185
|
-
timeout: 15000,
|
|
186
|
-
});
|
|
240
|
+
async _fetchViewSource(url, ctx) {
|
|
241
|
+
const slots = ctx === this._fallbackContext ? this._fallbackSlots : this.slots;
|
|
242
|
+
const slot = slots[this.slotIdx % slots.length];
|
|
243
|
+
this.slotIdx++;
|
|
187
244
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
245
|
+
try {
|
|
246
|
+
return await slot.lock.run(async () => {
|
|
247
|
+
const page = await this._ensurePage(slot, ctx);
|
|
248
|
+
slot.requestCount++;
|
|
249
|
+
if (ctx === this.context && this.isWarmExpired()) {
|
|
250
|
+
await this.warmWaf();
|
|
251
|
+
}
|
|
252
|
+
await page.goto(url, {
|
|
253
|
+
waitUntil: "domcontentloaded",
|
|
254
|
+
timeout: 15000,
|
|
255
|
+
});
|
|
256
|
+
await delay(1500);
|
|
257
|
+
return await page.content();
|
|
194
258
|
});
|
|
195
|
-
|
|
196
|
-
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.error(`[TikTokScraper] _fetchViewSource failed: ${e.message}`);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
197
264
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.catch(() => {});
|
|
265
|
+
async getUserInfo(username) {
|
|
266
|
+
const normalizedUsername = username.startsWith("@")
|
|
267
|
+
? username.slice(1)
|
|
268
|
+
: username;
|
|
269
|
+
const url = `https://www.tiktok.com/@${normalizedUsername}`;
|
|
270
|
+
const executablePath = detectBrowser();
|
|
205
271
|
|
|
206
|
-
|
|
272
|
+
// 第一趟:无登录态
|
|
273
|
+
let rawHtml = await this._fetchViewSource(url, this.context);
|
|
274
|
+
let result = rawHtml ? parseUserInfo(rawHtml) : null;
|
|
207
275
|
|
|
208
|
-
|
|
209
|
-
|
|
276
|
+
// 解析失败:fallback 到登录态
|
|
277
|
+
if (!result) {
|
|
278
|
+
await this._ensureFallback(executablePath);
|
|
279
|
+
rawHtml = await this._fetchViewSource(url, this._fallbackContext);
|
|
280
|
+
result = rawHtml ? parseUserInfo(rawHtml) : null;
|
|
281
|
+
}
|
|
210
282
|
|
|
211
|
-
|
|
212
|
-
const slot = this._pickSlot();
|
|
213
|
-
return slot.lock.run(async () => {
|
|
214
|
-
let rawHtml = await this._fetchViewSource(
|
|
215
|
-
`https://www.tiktok.com/@${uniqueId}`,
|
|
216
|
-
slot,
|
|
217
|
-
);
|
|
218
|
-
let result = parseUserInfo(rawHtml);
|
|
219
|
-
if (!result) {
|
|
220
|
-
try {
|
|
221
|
-
await this.warmWaf();
|
|
222
|
-
} catch {}
|
|
223
|
-
rawHtml = await this._fetchViewSource(
|
|
224
|
-
`https://www.tiktok.com/@${uniqueId}`,
|
|
225
|
-
slot,
|
|
226
|
-
);
|
|
227
|
-
result = parseUserInfo(rawHtml);
|
|
228
|
-
}
|
|
229
|
-
return result || null;
|
|
230
|
-
});
|
|
283
|
+
return result || null;
|
|
231
284
|
}
|
|
232
285
|
|
|
233
286
|
async getVideoInfo(videoUrl) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
287
|
+
const executablePath = detectBrowser();
|
|
288
|
+
|
|
289
|
+
// 第一趟:无登录态
|
|
290
|
+
let rawHtml = await this._fetchViewSource(videoUrl, this.context);
|
|
291
|
+
let result = rawHtml ? parseVideoInfo(rawHtml) : null;
|
|
292
|
+
|
|
293
|
+
// 解析失败:fallback 到登录态
|
|
294
|
+
if (!result) {
|
|
295
|
+
await this._ensureFallback(executablePath);
|
|
296
|
+
rawHtml = await this._fetchViewSource(videoUrl, this._fallbackContext);
|
|
297
|
+
result = rawHtml ? parseVideoInfo(rawHtml) : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result || null;
|
|
247
301
|
}
|
|
248
302
|
|
|
249
303
|
async getUserAndVideo(videoUrl) {
|
package/src/lib/url.js
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
const BASE_URL = 'https://www.tiktok.com';
|
|
2
|
-
|
|
3
|
-
export function extractUniqueId(url) {
|
|
4
|
-
const m = url.match(/\/@([\w.-]+)/);
|
|
5
|
-
return m ? m[1] : null;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function extractVideoId(url) {
|
|
9
|
-
const m = url.match(/\/video\/(\d+)/);
|
|
10
|
-
return m ? m[1] : null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function normalizeUsername(input) {
|
|
14
|
-
return (input || '').replace(/^@/, '');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function toProfileUrl(handle) {
|
|
18
|
-
const clean = normalizeUsername(handle);
|
|
19
|
-
return `${BASE_URL}/@${clean}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function toVideoUrl(handle, videoId) {
|
|
23
|
-
const clean = normalizeUsername(handle);
|
|
24
|
-
return `${BASE_URL}/@${clean}/video/${videoId}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ensureAbsoluteUrl(href) {
|
|
28
|
-
if (href.startsWith('http')) return href;
|
|
29
|
-
return `${BASE_URL}${href}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function isProfileUrl(url) {
|
|
33
|
-
return /\/@[\w.-]+(?:$|[?#])/.test(url);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isVideoUrl(url) {
|
|
37
|
-
return /\/video\/\d+/.test(url);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function extractDisplayPath(url) {
|
|
41
|
-
try {
|
|
42
|
-
const parts = new URL(url).pathname.split('/').filter(Boolean);
|
|
43
|
-
return parts.slice(-2).join('/');
|
|
44
|
-
} catch {
|
|
45
|
-
return url;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function extractAuthorFromVideoUrl(url) {
|
|
50
|
-
const m = url.match(/@([^/]+)\/video/);
|
|
51
|
-
return m ? '@' + m[1] : null;
|
|
52
|
-
}
|
|
1
|
+
const BASE_URL = 'https://www.tiktok.com';
|
|
2
|
+
|
|
3
|
+
export function extractUniqueId(url) {
|
|
4
|
+
const m = url.match(/\/@([\w.-]+)/);
|
|
5
|
+
return m ? m[1] : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function extractVideoId(url) {
|
|
9
|
+
const m = url.match(/\/video\/(\d+)/);
|
|
10
|
+
return m ? m[1] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeUsername(input) {
|
|
14
|
+
return (input || '').replace(/^@/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toProfileUrl(handle) {
|
|
18
|
+
const clean = normalizeUsername(handle);
|
|
19
|
+
return `${BASE_URL}/@${clean}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toVideoUrl(handle, videoId) {
|
|
23
|
+
const clean = normalizeUsername(handle);
|
|
24
|
+
return `${BASE_URL}/@${clean}/video/${videoId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ensureAbsoluteUrl(href) {
|
|
28
|
+
if (href.startsWith('http')) return href;
|
|
29
|
+
return `${BASE_URL}${href}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isProfileUrl(url) {
|
|
33
|
+
return /\/@[\w.-]+(?:$|[?#])/.test(url);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isVideoUrl(url) {
|
|
37
|
+
return /\/video\/\d+/.test(url);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extractDisplayPath(url) {
|
|
41
|
+
try {
|
|
42
|
+
const parts = new URL(url).pathname.split('/').filter(Boolean);
|
|
43
|
+
return parts.slice(-2).join('/');
|
|
44
|
+
} catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function extractAuthorFromVideoUrl(url) {
|
|
50
|
+
const m = url.match(/@([^/]+)\/video/);
|
|
51
|
+
return m ? '@' + m[1] : null;
|
|
52
|
+
}
|
package/src/main.js
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
|
-
import { parseArgs } from "./lib/args.js";
|
|
2
|
-
import { proxy, HELP_TEXT, getConfigText } from "./lib/constants.js";
|
|
3
|
-
import { handleInfo } from "./cli/info.js";
|
|
4
|
-
import { handleExplore } from "./cli/explore.js";
|
|
5
|
-
import { handleAttach } from "./cli/attach.js";
|
|
6
|
-
import { handleWatch } from "./cli/watch.js";
|
|
7
|
-
import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
|
|
8
|
-
import { handleOpen } from "./cli/open.js";
|
|
9
|
-
import { handleComments } from "./cli/comments.js";
|
|
10
|
-
import { handleVideoStats } from "./cli/videostats.js";
|
|
11
|
-
import { handleDbImport } from "./cli/db-import.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (exploreCount
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
import { parseArgs } from "./lib/args.js";
|
|
2
|
+
import { proxy, HELP_TEXT, getConfigText } from "./lib/constants.js";
|
|
3
|
+
import { handleInfo } from "./cli/info.js";
|
|
4
|
+
import { handleExplore } from "./cli/explore.js";
|
|
5
|
+
import { handleAttach } from "./cli/attach.js";
|
|
6
|
+
import { handleWatch } from "./cli/watch.js";
|
|
7
|
+
import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
|
|
8
|
+
import { handleOpen } from "./cli/open.js";
|
|
9
|
+
import { handleComments } from "./cli/comments.js";
|
|
10
|
+
import { handleVideoStats } from "./cli/videostats.js";
|
|
11
|
+
import { handleDbImport } from "./cli/db-import.js";
|
|
12
|
+
import { handleWebserver } from "./cli/webserver.js";
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const parsed = parseArgs();
|
|
16
|
+
|
|
17
|
+
switch (parsed.subcommand) {
|
|
18
|
+
case "explore":
|
|
19
|
+
return handleExplore(parsed);
|
|
20
|
+
case "info":
|
|
21
|
+
return handleInfo(parsed);
|
|
22
|
+
case "attach":
|
|
23
|
+
return handleAttach(parsed);
|
|
24
|
+
case "watch":
|
|
25
|
+
return handleWatch(parsed);
|
|
26
|
+
case "webserver":
|
|
27
|
+
return handleWebserver(parsed);
|
|
28
|
+
case "open":
|
|
29
|
+
return handleOpen(parsed);
|
|
30
|
+
case "comments":
|
|
31
|
+
return handleComments(parsed);
|
|
32
|
+
case "videostats":
|
|
33
|
+
return handleVideoStats(parsed);
|
|
34
|
+
case "db-import":
|
|
35
|
+
return handleDbImport(parsed);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
urls,
|
|
40
|
+
outputFile,
|
|
41
|
+
outputFormat,
|
|
42
|
+
exploreCount,
|
|
43
|
+
showConfig: showCfg,
|
|
44
|
+
showHelp,
|
|
45
|
+
showVersion,
|
|
46
|
+
customProxy,
|
|
47
|
+
configAction,
|
|
48
|
+
configKey,
|
|
49
|
+
configValue,
|
|
50
|
+
} = parsed;
|
|
51
|
+
|
|
52
|
+
if (showVersion) {
|
|
53
|
+
console.log(version);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
if (showHelp) return showUsage();
|
|
57
|
+
if (configAction) return handleConfig(configAction, configKey, configValue);
|
|
58
|
+
if (showCfg) return showConfig(urls, outputFile);
|
|
59
|
+
if (urls.length === 0 && exploreCount === 0) return showUsage();
|
|
60
|
+
|
|
61
|
+
// 默认行为:URL 走 info,--explore 走 explore
|
|
62
|
+
if (exploreCount > 0) {
|
|
63
|
+
return handleExplore({ ...parsed, subcommand: "explore" });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 有 URL 默认走 info
|
|
67
|
+
return handleInfo(parsed);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main().catch((err) => {
|
|
71
|
+
console.error(`错误: ${err.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|