@jackwener/opencli 0.9.5 → 0.9.8
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/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
- package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/workflows/ci.yml +14 -8
- package/.github/workflows/e2e-headed.yml +6 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release-please.yml +25 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security.yml +36 -0
- package/CLI-ELECTRON.md +89 -36
- package/CONTRIBUTING.md +167 -0
- package/README.md +98 -32
- package/README.zh-CN.md +99 -33
- package/dist/browser/discover.js +22 -7
- package/dist/browser.test.js +23 -0
- package/dist/build-manifest.d.ts +26 -0
- package/dist/build-manifest.js +132 -60
- package/dist/build-manifest.test.d.ts +1 -0
- package/dist/build-manifest.test.js +26 -0
- package/dist/cli-manifest.json +1875 -271
- package/dist/clis/antigravity/model.js +2 -2
- package/dist/clis/antigravity/send.js +2 -2
- package/dist/clis/bilibili/download.d.ts +10 -0
- package/dist/clis/bilibili/download.js +135 -0
- package/dist/clis/chatgpt/ask.d.ts +1 -0
- package/dist/clis/chatgpt/ask.js +68 -0
- package/dist/clis/chatgpt/send.js +11 -0
- package/dist/clis/chatwise/ask.d.ts +1 -0
- package/dist/clis/chatwise/ask.js +76 -0
- package/dist/clis/chatwise/export.d.ts +1 -0
- package/dist/clis/chatwise/export.js +46 -0
- package/dist/clis/chatwise/history.d.ts +1 -0
- package/dist/clis/chatwise/history.js +43 -0
- package/dist/clis/chatwise/model.d.ts +1 -0
- package/dist/clis/chatwise/model.js +81 -0
- package/dist/clis/chatwise/new.d.ts +1 -0
- package/dist/clis/chatwise/new.js +18 -0
- package/dist/clis/chatwise/read.d.ts +1 -0
- package/dist/clis/chatwise/read.js +39 -0
- package/dist/clis/chatwise/screenshot.d.ts +1 -0
- package/dist/clis/chatwise/screenshot.js +27 -0
- package/dist/clis/chatwise/send.d.ts +1 -0
- package/dist/clis/chatwise/send.js +45 -0
- package/dist/clis/chatwise/status.d.ts +1 -0
- package/dist/clis/chatwise/status.js +22 -0
- package/dist/clis/codex/ask.d.ts +1 -0
- package/dist/clis/codex/ask.js +67 -0
- package/dist/clis/codex/export.d.ts +1 -0
- package/dist/clis/codex/export.js +37 -0
- package/dist/clis/codex/history.d.ts +1 -0
- package/dist/clis/codex/history.js +43 -0
- package/dist/clis/codex/read.js +3 -5
- package/dist/clis/codex/screenshot.d.ts +1 -0
- package/dist/clis/codex/screenshot.js +27 -0
- package/dist/clis/codex/send.js +3 -6
- package/dist/clis/codex/status.js +2 -1
- package/dist/clis/cursor/ask.d.ts +1 -0
- package/dist/clis/cursor/ask.js +69 -0
- package/dist/clis/cursor/composer.js +9 -28
- package/dist/clis/cursor/export.d.ts +1 -0
- package/dist/clis/cursor/export.js +51 -0
- package/dist/clis/cursor/history.d.ts +1 -0
- package/dist/clis/cursor/history.js +43 -0
- package/dist/clis/cursor/new.js +4 -13
- package/dist/clis/cursor/screenshot.d.ts +1 -0
- package/dist/clis/cursor/screenshot.js +31 -0
- package/dist/clis/discord-app/channels.d.ts +1 -0
- package/dist/clis/discord-app/channels.js +45 -0
- package/dist/clis/discord-app/members.d.ts +1 -0
- package/dist/clis/discord-app/members.js +38 -0
- package/dist/clis/discord-app/read.d.ts +1 -0
- package/dist/clis/discord-app/read.js +45 -0
- package/dist/clis/discord-app/search.d.ts +1 -0
- package/dist/clis/discord-app/search.js +56 -0
- package/dist/clis/discord-app/send.d.ts +1 -0
- package/dist/clis/discord-app/send.js +27 -0
- package/dist/clis/discord-app/servers.d.ts +1 -0
- package/dist/clis/discord-app/servers.js +36 -0
- package/dist/clis/discord-app/status.d.ts +1 -0
- package/dist/clis/discord-app/status.js +16 -0
- package/dist/clis/feishu/new.d.ts +1 -0
- package/dist/clis/feishu/new.js +27 -0
- package/dist/clis/feishu/read.d.ts +1 -0
- package/dist/clis/feishu/read.js +40 -0
- package/dist/clis/feishu/search.d.ts +1 -0
- package/dist/clis/feishu/search.js +30 -0
- package/dist/clis/feishu/send.d.ts +1 -0
- package/dist/clis/feishu/send.js +39 -0
- package/dist/clis/feishu/status.d.ts +1 -0
- package/dist/clis/feishu/status.js +28 -0
- package/dist/clis/grok/ask.d.ts +1 -0
- package/dist/clis/grok/ask.js +82 -0
- package/dist/clis/grok/debug.d.ts +1 -0
- package/dist/clis/grok/debug.js +45 -0
- package/dist/clis/jimeng/generate.yaml +84 -0
- package/dist/clis/jimeng/history.yaml +47 -0
- package/dist/clis/linux-do/categories.yaml +41 -0
- package/dist/clis/linux-do/category.yaml +49 -0
- package/dist/clis/linux-do/hot.yaml +50 -0
- package/dist/clis/linux-do/latest.yaml +40 -0
- package/dist/clis/linux-do/search.yaml +45 -0
- package/dist/clis/linux-do/topic.yaml +38 -0
- package/dist/clis/notion/export.d.ts +1 -0
- package/dist/clis/notion/export.js +31 -0
- package/dist/clis/notion/favorites.d.ts +1 -0
- package/dist/clis/notion/favorites.js +84 -0
- package/dist/clis/notion/new.d.ts +1 -0
- package/dist/clis/notion/new.js +34 -0
- package/dist/clis/notion/read.d.ts +1 -0
- package/dist/clis/notion/read.js +30 -0
- package/dist/clis/notion/search.d.ts +1 -0
- package/dist/clis/notion/search.js +46 -0
- package/dist/clis/notion/sidebar.d.ts +1 -0
- package/dist/clis/notion/sidebar.js +41 -0
- package/dist/clis/notion/status.d.ts +1 -0
- package/dist/clis/notion/status.js +16 -0
- package/dist/clis/notion/write.d.ts +1 -0
- package/dist/clis/notion/write.js +40 -0
- package/dist/clis/twitter/download.d.ts +8 -0
- package/dist/clis/twitter/download.js +204 -0
- package/dist/clis/wechat/chats.d.ts +1 -0
- package/dist/clis/wechat/chats.js +28 -0
- package/dist/clis/wechat/contacts.d.ts +1 -0
- package/dist/clis/wechat/contacts.js +28 -0
- package/dist/clis/wechat/read.d.ts +1 -0
- package/dist/clis/wechat/read.js +58 -0
- package/dist/clis/wechat/search.d.ts +1 -0
- package/dist/clis/wechat/search.js +31 -0
- package/dist/clis/wechat/send.d.ts +1 -0
- package/dist/clis/wechat/send.js +42 -0
- package/dist/clis/wechat/status.d.ts +1 -0
- package/dist/clis/wechat/status.js +29 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
- package/dist/clis/xiaohongshu/creator-notes.js +109 -0
- package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-profile.js +54 -0
- package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-stats.js +74 -0
- package/dist/clis/xiaohongshu/download.d.ts +7 -0
- package/dist/clis/xiaohongshu/download.js +155 -0
- package/dist/clis/xiaohongshu/search.js +1 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
- package/dist/clis/xiaohongshu/user-helpers.js +67 -0
- package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
- package/dist/clis/xiaohongshu/user.js +46 -29
- package/dist/clis/zhihu/download.d.ts +11 -0
- package/dist/clis/zhihu/download.js +186 -0
- package/dist/clis/zhihu/download.test.d.ts +1 -0
- package/dist/clis/zhihu/download.test.js +10 -0
- package/dist/download/index.d.ts +79 -0
- package/dist/download/index.js +325 -0
- package/dist/download/progress.d.ts +36 -0
- package/dist/download/progress.js +111 -0
- package/dist/engine.test.js +15 -0
- package/dist/main.js +16 -3
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/steps/download.d.ts +34 -0
- package/dist/pipeline/steps/download.js +251 -0
- package/dist/pipeline/template.js +28 -0
- package/package.json +4 -3
- package/scripts/test-site.mjs +70 -0
- package/src/browser/discover.ts +23 -7
- package/src/browser.test.ts +23 -0
- package/src/build-manifest.test.ts +28 -0
- package/src/build-manifest.ts +147 -57
- package/src/clis/antigravity/README.md +2 -3
- package/src/clis/antigravity/README.zh-CN.md +2 -3
- package/src/clis/antigravity/SKILL.md +1 -1
- package/src/clis/antigravity/model.ts +2 -2
- package/src/clis/antigravity/send.ts +2 -2
- package/src/clis/bilibili/download.ts +161 -0
- package/src/clis/chatgpt/README.md +25 -16
- package/src/clis/chatgpt/README.zh-CN.md +27 -18
- package/src/clis/chatgpt/ask.ts +77 -0
- package/src/clis/chatgpt/send.ts +12 -0
- package/src/clis/chatwise/README.md +38 -0
- package/src/clis/chatwise/README.zh-CN.md +38 -0
- package/src/clis/chatwise/ask.ts +87 -0
- package/src/clis/chatwise/export.ts +51 -0
- package/src/clis/chatwise/history.ts +47 -0
- package/src/clis/chatwise/model.ts +87 -0
- package/src/clis/chatwise/new.ts +21 -0
- package/src/clis/chatwise/read.ts +42 -0
- package/src/clis/chatwise/screenshot.ts +33 -0
- package/src/clis/chatwise/send.ts +50 -0
- package/src/clis/chatwise/status.ts +25 -0
- package/src/clis/codex/ask.ts +77 -0
- package/src/clis/codex/export.ts +42 -0
- package/src/clis/codex/extract-diff.ts +1 -0
- package/src/clis/codex/history.ts +47 -0
- package/src/clis/codex/read.ts +5 -6
- package/src/clis/codex/screenshot.ts +33 -0
- package/src/clis/codex/send.ts +6 -7
- package/src/clis/codex/status.ts +4 -2
- package/src/clis/cursor/ask.ts +81 -0
- package/src/clis/cursor/composer.ts +9 -30
- package/src/clis/cursor/export.ts +57 -0
- package/src/clis/cursor/history.ts +47 -0
- package/src/clis/cursor/new.ts +4 -15
- package/src/clis/cursor/screenshot.ts +38 -0
- package/src/clis/discord-app/README.md +28 -0
- package/src/clis/discord-app/README.zh-CN.md +28 -0
- package/src/clis/discord-app/channels.ts +48 -0
- package/src/clis/discord-app/members.ts +41 -0
- package/src/clis/discord-app/read.ts +49 -0
- package/src/clis/discord-app/search.ts +64 -0
- package/src/clis/discord-app/send.ts +32 -0
- package/src/clis/discord-app/servers.ts +39 -0
- package/src/clis/discord-app/status.ts +18 -0
- package/src/clis/feishu/README.md +20 -0
- package/src/clis/feishu/README.zh-CN.md +20 -0
- package/src/clis/feishu/new.ts +32 -0
- package/src/clis/feishu/read.ts +48 -0
- package/src/clis/feishu/search.ts +35 -0
- package/src/clis/feishu/send.ts +46 -0
- package/src/clis/feishu/status.ts +34 -0
- package/src/clis/grok/ask.ts +90 -0
- package/src/clis/grok/debug.ts +49 -0
- package/src/clis/jimeng/generate.yaml +84 -0
- package/src/clis/jimeng/history.yaml +47 -0
- package/src/clis/linux-do/categories.yaml +41 -0
- package/src/clis/linux-do/category.yaml +49 -0
- package/src/clis/linux-do/hot.yaml +50 -0
- package/src/clis/linux-do/latest.yaml +40 -0
- package/src/clis/linux-do/search.yaml +45 -0
- package/src/clis/linux-do/topic.yaml +38 -0
- package/src/clis/notion/README.md +29 -0
- package/src/clis/notion/README.zh-CN.md +29 -0
- package/src/clis/notion/export.ts +36 -0
- package/src/clis/notion/favorites.ts +87 -0
- package/src/clis/notion/new.ts +39 -0
- package/src/clis/notion/read.ts +33 -0
- package/src/clis/notion/search.ts +54 -0
- package/src/clis/notion/sidebar.ts +44 -0
- package/src/clis/notion/status.ts +18 -0
- package/src/clis/notion/write.ts +45 -0
- package/src/clis/twitter/download.ts +227 -0
- package/src/clis/wechat/README.md +28 -0
- package/src/clis/wechat/README.zh-CN.md +28 -0
- package/src/clis/wechat/chats.ts +33 -0
- package/src/clis/wechat/contacts.ts +33 -0
- package/src/clis/wechat/read.ts +72 -0
- package/src/clis/wechat/search.ts +36 -0
- package/src/clis/wechat/send.ts +49 -0
- package/src/clis/wechat/status.ts +35 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
- package/src/clis/xiaohongshu/creator-notes.ts +116 -0
- package/src/clis/xiaohongshu/creator-profile.ts +60 -0
- package/src/clis/xiaohongshu/creator-stats.ts +81 -0
- package/src/clis/xiaohongshu/download.ts +173 -0
- package/src/clis/xiaohongshu/search.ts +1 -1
- package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
- package/src/clis/xiaohongshu/user-helpers.ts +85 -0
- package/src/clis/xiaohongshu/user.ts +52 -32
- package/src/clis/zhihu/download.test.ts +12 -0
- package/src/clis/zhihu/download.ts +223 -0
- package/src/download/index.ts +395 -0
- package/src/download/progress.ts +125 -0
- package/src/engine.test.ts +17 -0
- package/src/main.ts +12 -3
- package/src/pipeline/registry.ts +2 -0
- package/src/pipeline/steps/download.ts +310 -0
- package/src/pipeline/template.ts +26 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: download — file download with concurrency and progress.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Direct HTTP downloads (images, documents)
|
|
6
|
+
* - yt-dlp integration for video platforms
|
|
7
|
+
* - Browser cookie forwarding for authenticated downloads
|
|
8
|
+
* - Filename templating and deduplication
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
import type { IPage } from '../../types.js';
|
|
15
|
+
import { render } from '../template.js';
|
|
16
|
+
import {
|
|
17
|
+
httpDownload,
|
|
18
|
+
ytdlpDownload,
|
|
19
|
+
saveDocument,
|
|
20
|
+
detectContentType,
|
|
21
|
+
requiresYtdlp,
|
|
22
|
+
sanitizeFilename,
|
|
23
|
+
generateFilename,
|
|
24
|
+
exportCookiesToNetscape,
|
|
25
|
+
getTempDir,
|
|
26
|
+
} from '../../download/index.js';
|
|
27
|
+
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
28
|
+
|
|
29
|
+
export interface DownloadResult {
|
|
30
|
+
status: 'success' | 'skipped' | 'failed';
|
|
31
|
+
path?: string;
|
|
32
|
+
size?: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
duration?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple async concurrency limiter for downloads.
|
|
39
|
+
*/
|
|
40
|
+
async function mapConcurrent<T, R>(
|
|
41
|
+
items: T[],
|
|
42
|
+
limit: number,
|
|
43
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
44
|
+
): Promise<R[]> {
|
|
45
|
+
const results: R[] = new Array(items.length);
|
|
46
|
+
let index = 0;
|
|
47
|
+
|
|
48
|
+
async function worker() {
|
|
49
|
+
while (index < items.length) {
|
|
50
|
+
const i = index++;
|
|
51
|
+
results[i] = await fn(items[i], i);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
56
|
+
await Promise.all(workers);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract cookies from browser page.
|
|
62
|
+
*/
|
|
63
|
+
async function extractBrowserCookies(page: IPage, domain?: string): Promise<string> {
|
|
64
|
+
try {
|
|
65
|
+
// Use browser evaluate to get document.cookie
|
|
66
|
+
const cookieString = await page.evaluate(`(() => document.cookie)()`);
|
|
67
|
+
return typeof cookieString === 'string' ? cookieString : '';
|
|
68
|
+
} catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract cookies as array for yt-dlp Netscape format.
|
|
75
|
+
*/
|
|
76
|
+
async function extractCookiesArray(
|
|
77
|
+
page: IPage,
|
|
78
|
+
domain: string,
|
|
79
|
+
): Promise<Array<{ name: string; value: string; domain: string; path: string; secure: boolean; httpOnly: boolean }>> {
|
|
80
|
+
try {
|
|
81
|
+
const cookieString = await extractBrowserCookies(page);
|
|
82
|
+
if (!cookieString) return [];
|
|
83
|
+
|
|
84
|
+
return cookieString.split(';').map((c) => {
|
|
85
|
+
const [name, ...rest] = c.trim().split('=');
|
|
86
|
+
return {
|
|
87
|
+
name: name || '',
|
|
88
|
+
value: rest.join('=') || '',
|
|
89
|
+
domain,
|
|
90
|
+
path: '/',
|
|
91
|
+
secure: true,
|
|
92
|
+
httpOnly: false,
|
|
93
|
+
};
|
|
94
|
+
}).filter((c) => c.name);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Download step handler for YAML pipelines.
|
|
102
|
+
*
|
|
103
|
+
* Usage in YAML:
|
|
104
|
+
* ```yaml
|
|
105
|
+
* pipeline:
|
|
106
|
+
* - download:
|
|
107
|
+
* url: ${{ item.imageUrl }}
|
|
108
|
+
* dir: ./downloads
|
|
109
|
+
* filename: ${{ item.title }}.jpg
|
|
110
|
+
* concurrency: 5
|
|
111
|
+
* skip_existing: true
|
|
112
|
+
* use_ytdlp: false
|
|
113
|
+
* type: auto
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export async function stepDownload(
|
|
117
|
+
page: IPage | null,
|
|
118
|
+
params: any,
|
|
119
|
+
data: any,
|
|
120
|
+
args: Record<string, any>,
|
|
121
|
+
): Promise<any> {
|
|
122
|
+
// Parse parameters with defaults
|
|
123
|
+
const urlTemplate = typeof params === 'string' ? params : (params?.url ?? '');
|
|
124
|
+
const dirTemplate = params?.dir ?? './downloads';
|
|
125
|
+
const filenameTemplate = params?.filename ?? '';
|
|
126
|
+
const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 3;
|
|
127
|
+
const skipExisting = params?.skip_existing !== false;
|
|
128
|
+
const timeout = typeof params?.timeout === 'number' ? params.timeout * 1000 : 30000;
|
|
129
|
+
const useYtdlp = params?.use_ytdlp ?? false;
|
|
130
|
+
const ytdlpArgs = Array.isArray(params?.ytdlp_args) ? params.ytdlp_args : [];
|
|
131
|
+
const contentType = params?.type ?? 'auto';
|
|
132
|
+
const showProgress = params?.progress !== false;
|
|
133
|
+
const contentTemplate = params?.content;
|
|
134
|
+
const metadataTemplate = params?.metadata;
|
|
135
|
+
|
|
136
|
+
// Resolve output directory
|
|
137
|
+
const dir = String(render(dirTemplate, { args, data }));
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
139
|
+
|
|
140
|
+
// Normalize data to array
|
|
141
|
+
const items: any[] = Array.isArray(data) ? data : data ? [data] : [];
|
|
142
|
+
if (items.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Create progress tracker
|
|
147
|
+
const tracker = new DownloadProgressTracker(items.length, showProgress);
|
|
148
|
+
|
|
149
|
+
// Extract cookies if browser is available
|
|
150
|
+
let cookies = '';
|
|
151
|
+
let cookiesFile: string | undefined;
|
|
152
|
+
|
|
153
|
+
if (page) {
|
|
154
|
+
cookies = await extractBrowserCookies(page);
|
|
155
|
+
|
|
156
|
+
// For yt-dlp, we need to export cookies to Netscape format
|
|
157
|
+
if (useYtdlp || items.some((item, index) => {
|
|
158
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
159
|
+
return requiresYtdlp(url);
|
|
160
|
+
})) {
|
|
161
|
+
try {
|
|
162
|
+
// Try to get domain from first URL
|
|
163
|
+
const firstUrl = String(render(urlTemplate, { args, data, item: items[0], index: 0 }));
|
|
164
|
+
const domain = new URL(firstUrl).hostname;
|
|
165
|
+
const cookiesArray = await extractCookiesArray(page, domain);
|
|
166
|
+
|
|
167
|
+
if (cookiesArray.length > 0) {
|
|
168
|
+
const tempDir = getTempDir();
|
|
169
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
170
|
+
cookiesFile = path.join(tempDir, `cookies_${Date.now()}.txt`);
|
|
171
|
+
exportCookiesToNetscape(cookiesArray, cookiesFile);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Ignore cookie extraction errors
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Process downloads with concurrency
|
|
180
|
+
const results = await mapConcurrent(items, concurrency, async (item, index): Promise<any> => {
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
|
|
183
|
+
// Render URL
|
|
184
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
185
|
+
if (!url) {
|
|
186
|
+
tracker.onFileComplete(false);
|
|
187
|
+
return {
|
|
188
|
+
...item,
|
|
189
|
+
_download: { status: 'failed', error: 'Empty URL' } as DownloadResult,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Render filename
|
|
194
|
+
let filename: string;
|
|
195
|
+
if (filenameTemplate) {
|
|
196
|
+
filename = String(render(filenameTemplate, { args, data, item, index }));
|
|
197
|
+
} else {
|
|
198
|
+
filename = generateFilename(url, index);
|
|
199
|
+
}
|
|
200
|
+
filename = sanitizeFilename(filename);
|
|
201
|
+
|
|
202
|
+
const destPath = path.join(dir, filename);
|
|
203
|
+
|
|
204
|
+
// Check if file exists and skip_existing is true
|
|
205
|
+
if (skipExisting && fs.existsSync(destPath)) {
|
|
206
|
+
tracker.onFileComplete(true, true);
|
|
207
|
+
return {
|
|
208
|
+
...item,
|
|
209
|
+
_download: {
|
|
210
|
+
status: 'skipped',
|
|
211
|
+
path: destPath,
|
|
212
|
+
size: fs.statSync(destPath).size,
|
|
213
|
+
} as DownloadResult,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create progress bar for this file
|
|
218
|
+
const progressBar = tracker.onFileStart(filename, index);
|
|
219
|
+
|
|
220
|
+
// Determine download method
|
|
221
|
+
const detectedType = contentType === 'auto' ? detectContentType(url) : contentType;
|
|
222
|
+
const shouldUseYtdlp = useYtdlp || (detectedType === 'video' && requiresYtdlp(url));
|
|
223
|
+
|
|
224
|
+
let result: { success: boolean; size: number; error?: string };
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (detectedType === 'document' && contentTemplate) {
|
|
228
|
+
// Save extracted content as document
|
|
229
|
+
const content = String(render(contentTemplate, { args, data, item, index }));
|
|
230
|
+
const metadata = metadataTemplate
|
|
231
|
+
? Object.fromEntries(
|
|
232
|
+
Object.entries(metadataTemplate).map(([k, v]) => [k, render(v, { args, data, item, index })]),
|
|
233
|
+
)
|
|
234
|
+
: undefined;
|
|
235
|
+
|
|
236
|
+
const ext = path.extname(filename).toLowerCase();
|
|
237
|
+
const format = ext === '.json' ? 'json' : ext === '.html' ? 'html' : 'markdown';
|
|
238
|
+
result = await saveDocument(content, destPath, format, metadata);
|
|
239
|
+
|
|
240
|
+
if (progressBar) {
|
|
241
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
242
|
+
}
|
|
243
|
+
} else if (shouldUseYtdlp) {
|
|
244
|
+
// Use yt-dlp for video downloads
|
|
245
|
+
result = await ytdlpDownload(url, destPath, {
|
|
246
|
+
cookiesFile,
|
|
247
|
+
extraArgs: ytdlpArgs,
|
|
248
|
+
onProgress: (percent) => {
|
|
249
|
+
if (progressBar) {
|
|
250
|
+
progressBar.update(percent, 100);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (progressBar) {
|
|
256
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// Direct HTTP download
|
|
260
|
+
result = await httpDownload(url, destPath, {
|
|
261
|
+
cookies,
|
|
262
|
+
timeout,
|
|
263
|
+
onProgress: (received, total) => {
|
|
264
|
+
if (progressBar) {
|
|
265
|
+
progressBar.update(received, total);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (progressBar) {
|
|
271
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (err: any) {
|
|
275
|
+
result = { success: false, size: 0, error: err.message };
|
|
276
|
+
if (progressBar) {
|
|
277
|
+
progressBar.fail(err.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
tracker.onFileComplete(result.success);
|
|
282
|
+
|
|
283
|
+
const duration = Date.now() - startTime;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...item,
|
|
287
|
+
_download: {
|
|
288
|
+
status: result.success ? 'success' : 'failed',
|
|
289
|
+
path: result.success ? destPath : undefined,
|
|
290
|
+
size: result.size,
|
|
291
|
+
error: result.error,
|
|
292
|
+
duration,
|
|
293
|
+
} as DownloadResult,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Cleanup temp cookie file
|
|
298
|
+
if (cookiesFile && fs.existsSync(cookiesFile)) {
|
|
299
|
+
try {
|
|
300
|
+
fs.unlinkSync(cookiesFile);
|
|
301
|
+
} catch {
|
|
302
|
+
// Ignore cleanup errors
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Show summary
|
|
307
|
+
tracker.finish();
|
|
308
|
+
|
|
309
|
+
return results;
|
|
310
|
+
}
|
package/src/pipeline/template.ts
CHANGED
|
@@ -119,6 +119,32 @@ function applyFilter(filterExpr: string, value: any): any {
|
|
|
119
119
|
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
120
120
|
case 'json':
|
|
121
121
|
return JSON.stringify(value ?? null);
|
|
122
|
+
case 'slugify':
|
|
123
|
+
// Convert to URL-safe slug
|
|
124
|
+
return typeof value === 'string'
|
|
125
|
+
? value
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
128
|
+
.replace(/^-|-$/g, '')
|
|
129
|
+
: value;
|
|
130
|
+
case 'sanitize':
|
|
131
|
+
// Remove invalid filename characters
|
|
132
|
+
return typeof value === 'string'
|
|
133
|
+
? value.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
|
|
134
|
+
: value;
|
|
135
|
+
case 'ext': {
|
|
136
|
+
// Extract file extension from URL or path
|
|
137
|
+
if (typeof value !== 'string') return value;
|
|
138
|
+
const lastDot = value.lastIndexOf('.');
|
|
139
|
+
const lastSlash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
|
140
|
+
return lastDot > lastSlash ? value.slice(lastDot) : '';
|
|
141
|
+
}
|
|
142
|
+
case 'basename': {
|
|
143
|
+
// Extract filename from URL or path
|
|
144
|
+
if (typeof value !== 'string') return value;
|
|
145
|
+
const parts = value.split(/[/\\]/);
|
|
146
|
+
return parts[parts.length - 1] || value;
|
|
147
|
+
}
|
|
122
148
|
default:
|
|
123
149
|
return value;
|
|
124
150
|
}
|
|
@@ -79,6 +79,31 @@ describe('login-required commands — graceful failure', () => {
|
|
|
79
79
|
await expectGracefulAuthFailure(['xueqiu', 'watchlist', '-f', 'json'], 'xueqiu watchlist');
|
|
80
80
|
}, 60_000);
|
|
81
81
|
|
|
82
|
+
// ── linux-do (requires login — all endpoints need authentication) ──
|
|
83
|
+
it('linux-do hot fails gracefully without login', async () => {
|
|
84
|
+
await expectGracefulAuthFailure(['linux-do', 'hot', '--limit', '3', '-f', 'json'], 'linux-do hot');
|
|
85
|
+
}, 60_000);
|
|
86
|
+
|
|
87
|
+
it('linux-do latest fails gracefully without login', async () => {
|
|
88
|
+
await expectGracefulAuthFailure(['linux-do', 'latest', '--limit', '3', '-f', 'json'], 'linux-do latest');
|
|
89
|
+
}, 60_000);
|
|
90
|
+
|
|
91
|
+
it('linux-do categories fails gracefully without login', async () => {
|
|
92
|
+
await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json'], 'linux-do categories');
|
|
93
|
+
}, 60_000);
|
|
94
|
+
|
|
95
|
+
it('linux-do category fails gracefully without login', async () => {
|
|
96
|
+
await expectGracefulAuthFailure(['linux-do', 'category', '--slug', 'general', '--id', '1', '--limit', '3', '-f', 'json'], 'linux-do category');
|
|
97
|
+
}, 60_000);
|
|
98
|
+
|
|
99
|
+
it('linux-do topic fails gracefully without login', async () => {
|
|
100
|
+
await expectGracefulAuthFailure(['linux-do', 'topic', '--id', '1', '-f', 'json'], 'linux-do topic');
|
|
101
|
+
}, 60_000);
|
|
102
|
+
|
|
103
|
+
it('linux-do search fails gracefully without login', async () => {
|
|
104
|
+
await expectGracefulAuthFailure(['linux-do', 'search', '--keyword', 'test', '--limit', '3', '-f', 'json'], 'linux-do search');
|
|
105
|
+
}, 60_000);
|
|
106
|
+
|
|
82
107
|
// ── xiaohongshu (requires login) ──
|
|
83
108
|
it('xiaohongshu feed fails gracefully without login', async () => {
|
|
84
109
|
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json'], 'xiaohongshu feed');
|