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.
- 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 +173 -105
- 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,28 @@ 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 = 9999;
|
|
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
|
+
|
|
28
|
+
function fallbackProfileExists() {
|
|
29
|
+
const fs = require("fs");
|
|
30
|
+
const dir = getFallbackProfileDir();
|
|
31
|
+
return fs.existsSync(dir);
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
class PageSlot {
|
|
16
35
|
constructor(page) {
|
|
17
36
|
this.page = page;
|
|
@@ -47,6 +66,46 @@ class PromiseQueue {
|
|
|
47
66
|
}
|
|
48
67
|
}
|
|
49
68
|
|
|
69
|
+
function createLaunchOptions(executablePath) {
|
|
70
|
+
return {
|
|
71
|
+
headless: true,
|
|
72
|
+
executablePath,
|
|
73
|
+
handleSIGINT: false,
|
|
74
|
+
handleSIGTERM: false,
|
|
75
|
+
handleSIGHUP: false,
|
|
76
|
+
args: [
|
|
77
|
+
"--no-sandbox",
|
|
78
|
+
"--disable-setuid-sandbox",
|
|
79
|
+
"--disable-dev-shm-usage",
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function initContext(executablePath, poolSize, userDataDir) {
|
|
85
|
+
let context;
|
|
86
|
+
let browser = null;
|
|
87
|
+
const slots = [];
|
|
88
|
+
|
|
89
|
+
if (userDataDir) {
|
|
90
|
+
context = await chromium.launchPersistentContext(userDataDir, createLaunchOptions(executablePath));
|
|
91
|
+
const existing = context.pages();
|
|
92
|
+
if (existing.length > 0) {
|
|
93
|
+
slots.push(new PageSlot(existing[0]));
|
|
94
|
+
}
|
|
95
|
+
for (let i = slots.length; i < poolSize; i++) {
|
|
96
|
+
slots.push(new PageSlot(await context.newPage()));
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
browser = await chromium.launch(createLaunchOptions(executablePath));
|
|
100
|
+
context = await browser.newContext();
|
|
101
|
+
for (let i = 0; i < poolSize; i++) {
|
|
102
|
+
slots.push(new PageSlot(await context.newPage()));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { browser, context, slots };
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
export class TikTokScraper {
|
|
51
110
|
constructor({
|
|
52
111
|
poolSize = DEFAULT_POOL_SIZE,
|
|
@@ -64,6 +123,10 @@ export class TikTokScraper {
|
|
|
64
123
|
this.slotIdx = 0;
|
|
65
124
|
this.lastWarmTime = 0;
|
|
66
125
|
this.warmPromise = null;
|
|
126
|
+
this._fallbackBrowser = null;
|
|
127
|
+
this._fallbackContext = null;
|
|
128
|
+
this._fallbackSlots = [];
|
|
129
|
+
this._fallbackSlotIdx = 0;
|
|
67
130
|
}
|
|
68
131
|
|
|
69
132
|
async init() {
|
|
@@ -73,49 +136,60 @@ export class TikTokScraper {
|
|
|
73
136
|
"未找到本地浏览器(Chrome/Edge),请先安装浏览器或执行 npx playwright install",
|
|
74
137
|
);
|
|
75
138
|
}
|
|
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
|
-
}
|
|
139
|
+
const { browser, context, slots } = await initContext(executablePath, this.poolSize, null);
|
|
140
|
+
this.browser = browser;
|
|
141
|
+
this.context = context;
|
|
142
|
+
this.slots = slots;
|
|
92
143
|
await this.warmWaf();
|
|
93
144
|
}
|
|
94
145
|
|
|
146
|
+
async _ensureFallback(executablePath) {
|
|
147
|
+
if (this._fallbackContext) return;
|
|
148
|
+
if (!fallbackProfileExists()) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const fallbackDir = getFallbackProfileDir();
|
|
152
|
+
console.error(`[TikTokScraper] 无登录态获取失败,fallback 到登录态 profile: ${fallbackDir}`);
|
|
153
|
+
const { browser, context, slots } = await initContext(executablePath, 1, fallbackDir);
|
|
154
|
+
this._fallbackBrowser = browser;
|
|
155
|
+
this._fallbackContext = context;
|
|
156
|
+
this._fallbackSlots = slots;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
95
160
|
async close() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
161
|
+
const closeAll = async (browser) => {
|
|
162
|
+
if (browser) {
|
|
163
|
+
let closeTimedOut = false;
|
|
164
|
+
const closePromise = browser.close().catch((error) => {
|
|
165
|
+
console.error(
|
|
166
|
+
`[TikTokScraper] browser.close() failed: ${error.message}`,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
await Promise.race([
|
|
170
|
+
closePromise,
|
|
171
|
+
delay(BROWSER_CLOSE_TIMEOUT).then(() => { closeTimedOut = true; }),
|
|
172
|
+
]);
|
|
173
|
+
if (closeTimedOut) {
|
|
174
|
+
console.error(
|
|
175
|
+
`[TikTokScraper] browser.close() 超时 ${BROWSER_CLOSE_TIMEOUT}ms,跳过等待并继续退出`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
114
178
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 无登录态的 browser 是 chromium.launch() 返回的,直接 close
|
|
182
|
+
await closeAll(this.browser);
|
|
183
|
+
// fallback 的 context 是 launchPersistentContext 返回的,直接 close
|
|
184
|
+
if (this._fallbackContext) {
|
|
185
|
+
try { await this._fallbackContext.close(); } catch {}
|
|
118
186
|
}
|
|
187
|
+
this.browser = null;
|
|
188
|
+
this.context = null;
|
|
189
|
+
this.slots = [];
|
|
190
|
+
this._fallbackBrowser = null;
|
|
191
|
+
this._fallbackContext = null;
|
|
192
|
+
this._fallbackSlots = [];
|
|
119
193
|
}
|
|
120
194
|
|
|
121
195
|
async restart() {
|
|
@@ -125,7 +199,7 @@ export class TikTokScraper {
|
|
|
125
199
|
|
|
126
200
|
isAlive() {
|
|
127
201
|
try {
|
|
128
|
-
return !!(this.
|
|
202
|
+
return !!(this.context && (!this.browser || this.browser.isConnected()));
|
|
129
203
|
} catch {
|
|
130
204
|
return false;
|
|
131
205
|
}
|
|
@@ -152,98 +226,92 @@ export class TikTokScraper {
|
|
|
152
226
|
return this.warmPromise;
|
|
153
227
|
}
|
|
154
228
|
|
|
155
|
-
|
|
229
|
+
isWarmExpired() {
|
|
156
230
|
return Date.now() - this.lastWarmTime > this.wafTtl;
|
|
157
231
|
}
|
|
158
232
|
|
|
159
|
-
_pickSlot() {
|
|
160
|
-
const slot =
|
|
161
|
-
this.slotIdx++;
|
|
233
|
+
_pickSlot(slots, idx, setIdx) {
|
|
234
|
+
const slot = slots[idx % slots.length];
|
|
162
235
|
return slot;
|
|
163
236
|
}
|
|
164
237
|
|
|
165
|
-
async _ensurePage(slot) {
|
|
238
|
+
async _ensurePage(slot, context) {
|
|
166
239
|
try {
|
|
167
|
-
if (
|
|
168
|
-
!slot.page.isClosed() &&
|
|
169
|
-
slot.requestCount < this.maxRequestsPerPage
|
|
170
|
-
) {
|
|
240
|
+
if (!slot.page.isClosed() && slot.requestCount < this.maxRequestsPerPage) {
|
|
171
241
|
return slot.page;
|
|
172
242
|
}
|
|
173
243
|
} catch {}
|
|
174
244
|
await slot.page?.close().catch(() => {});
|
|
175
|
-
slot.page = await
|
|
245
|
+
slot.page = await context.newPage();
|
|
176
246
|
slot.requestCount = 0;
|
|
177
247
|
return slot.page;
|
|
178
248
|
}
|
|
179
249
|
|
|
180
|
-
async _fetchViewSource(url,
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
waitUntil: "domcontentloaded",
|
|
185
|
-
timeout: 15000,
|
|
186
|
-
});
|
|
250
|
+
async _fetchViewSource(url, ctx) {
|
|
251
|
+
const slots = ctx === this._fallbackContext ? this._fallbackSlots : this.slots;
|
|
252
|
+
const slot = slots[this.slotIdx % slots.length];
|
|
253
|
+
this.slotIdx++;
|
|
187
254
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
try {
|
|
256
|
+
return await slot.lock.run(async () => {
|
|
257
|
+
const page = await this._ensurePage(slot, ctx);
|
|
258
|
+
slot.requestCount++;
|
|
259
|
+
if (ctx === this.context && this.isWarmExpired()) {
|
|
260
|
+
await this.warmWaf();
|
|
261
|
+
}
|
|
262
|
+
await page.goto(url, {
|
|
263
|
+
waitUntil: "domcontentloaded",
|
|
264
|
+
timeout: 15000,
|
|
265
|
+
});
|
|
266
|
+
await delay(1500);
|
|
267
|
+
return await page.content();
|
|
194
268
|
});
|
|
195
|
-
|
|
196
|
-
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error(`[TikTokScraper] _fetchViewSource failed: ${e.message}`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
197
274
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.catch(() => {});
|
|
275
|
+
async getUserInfo(username) {
|
|
276
|
+
const normalizedUsername = username.startsWith("@")
|
|
277
|
+
? username.slice(1)
|
|
278
|
+
: username;
|
|
279
|
+
const url = `https://www.tiktok.com/@${normalizedUsername}`;
|
|
280
|
+
const executablePath = detectBrowser();
|
|
205
281
|
|
|
206
|
-
|
|
282
|
+
// 第一趟:无登录态
|
|
283
|
+
let rawHtml = await this._fetchViewSource(url, this.context);
|
|
284
|
+
let result = rawHtml ? parseUserInfo(rawHtml) : null;
|
|
207
285
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
286
|
+
// 解析失败:fallback 到登录态
|
|
287
|
+
if (!result) {
|
|
288
|
+
const ok = await this._ensureFallback(executablePath);
|
|
289
|
+
if (ok) {
|
|
290
|
+
rawHtml = await this._fetchViewSource(url, this._fallbackContext);
|
|
291
|
+
result = rawHtml ? parseUserInfo(rawHtml) : null;
|
|
228
292
|
}
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result || null;
|
|
231
296
|
}
|
|
232
297
|
|
|
233
298
|
async getVideoInfo(videoUrl) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
299
|
+
const executablePath = detectBrowser();
|
|
300
|
+
|
|
301
|
+
// 第一趟:无登录态
|
|
302
|
+
let rawHtml = await this._fetchViewSource(videoUrl, this.context);
|
|
303
|
+
let result = rawHtml ? parseVideoInfo(rawHtml) : null;
|
|
304
|
+
|
|
305
|
+
// 解析失败:fallback 到登录态
|
|
306
|
+
if (!result) {
|
|
307
|
+
const ok = await this._ensureFallback(executablePath);
|
|
308
|
+
if (ok) {
|
|
309
|
+
rawHtml = await this._fetchViewSource(videoUrl, this._fallbackContext);
|
|
310
|
+
result = rawHtml ? parseVideoInfo(rawHtml) : null;
|
|
244
311
|
}
|
|
245
|
-
|
|
246
|
-
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result || null;
|
|
247
315
|
}
|
|
248
316
|
|
|
249
317
|
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
|
+
});
|