@playcraft/cli 0.0.53 → 0.0.55
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/dist/commands/create.js +164 -98
- package/dist/index.js +4 -1
- package/dist/scaffold/templates.js +35 -0
- package/package.json +3 -3
- package/project-templates/playable-template-phaser/.playcraftignore +10 -0
- package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
- package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
- package/project-templates/playable-template-phaser/README.md +381 -0
- package/project-templates/playable-template-phaser/assets/brain.png +0 -0
- package/project-templates/playable-template-phaser/assets/button.png +0 -0
- package/project-templates/playable-template-phaser/assets/interact.png +0 -0
- package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
- package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
- package/project-templates/playable-template-phaser/builds.config.js +12 -0
- package/project-templates/playable-template-phaser/globals.d.ts +2 -0
- package/project-templates/playable-template-phaser/index.html +12 -0
- package/project-templates/playable-template-phaser/package.json +45 -0
- package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
- package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-phaser/src/Game.ts +154 -0
- package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
- package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
- package/project-templates/playable-template-phaser/src/index.css +18 -0
- package/project-templates/playable-template-phaser/src/index.html +9 -0
- package/project-templates/playable-template-phaser/src/index.ts +30 -0
- package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
- package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
- package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
- package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
- package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
- package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
- package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
- package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
- package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
- package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
- package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
- package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
- package/project-templates/playable-template-phaser/tsconfig.json +26 -0
- package/project-templates/playable-template-phaser/vite.config.ts +64 -0
- package/project-templates/playable-template-phaser//346/226/260/346/211/213/345/274/200/345/217/221/346/214/207/345/215/227.md +209 -0
- package/project-templates/playable-template-three/README.md +795 -0
- package/project-templates/playable-template-three/assets/Stork.glb +0 -0
- package/project-templates/playable-template-three/assets/button.png +0 -0
- package/project-templates/playable-template-three/assets/logo.webp +0 -0
- package/project-templates/playable-template-three/build.json +7 -0
- package/project-templates/playable-template-three/builds.config.example.js +151 -0
- package/project-templates/playable-template-three/builds.config.js +158 -0
- package/project-templates/playable-template-three/package.json +36 -0
- package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
- package/project-templates/playable-template-three/src/Game.ts +215 -0
- package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
- package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
- package/project-templates/playable-template-three/src/global.d.ts +108 -0
- package/project-templates/playable-template-three/src/index.css +18 -0
- package/project-templates/playable-template-three/src/index.html +9 -0
- package/project-templates/playable-template-three/src/index.ts +20 -0
- package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
- package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
- package/project-templates/playable-template-three/src/theme/README.md +225 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
- package/project-templates/playable-template-three/src/theme/current.ts +12 -0
- package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
- package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
- package/project-templates/playable-template-three/tsconfig.json +24 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
|
+
<title>Phaser Template - Playable Ads</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="game-container"></div>
|
|
10
|
+
<script type="module" src="/src/index.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playable-template-phaser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A starter template for creating playable ads using Phaser and @playcraft/adsdk with TypeScript support",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"dev:wp": "playable-scripts dev",
|
|
8
|
+
"build": "playable-scripts build",
|
|
9
|
+
"builds": "playable-scripts builds",
|
|
10
|
+
"pack-to-playcraft-template-zip": "playable-scripts pack",
|
|
11
|
+
"webp": "node scripts/convert-to-webp.js",
|
|
12
|
+
"webp:80": "node scripts/convert-to-webp.js 80"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"playable",
|
|
16
|
+
"ads",
|
|
17
|
+
"template",
|
|
18
|
+
"phaser",
|
|
19
|
+
"webgl",
|
|
20
|
+
"2d",
|
|
21
|
+
"typescript",
|
|
22
|
+
"game"
|
|
23
|
+
],
|
|
24
|
+
"author": "Smoud",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@playcraft/adsdk": "*",
|
|
27
|
+
"phaser": "^3.88.2",
|
|
28
|
+
"json5": "^2.2.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@playcraft/devkit": "*",
|
|
32
|
+
"@types/node": "^22.10.5",
|
|
33
|
+
"vite": "^5.4.0",
|
|
34
|
+
"typescript": "~5.7.2",
|
|
35
|
+
"prettier": "^2.7.1",
|
|
36
|
+
"babel-loader": "^10.0.0",
|
|
37
|
+
"css-loader": "^7.1.2",
|
|
38
|
+
"esbuild-loader": "^4.4.1",
|
|
39
|
+
"json5-loader": "^4.0.1",
|
|
40
|
+
"style-loader": "^4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=16.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 音频转 MP3 文件脚本
|
|
3
|
+
*
|
|
4
|
+
* 支持两种输入源:
|
|
5
|
+
* 1. 直接传入 base64 字符串
|
|
6
|
+
* 2. 从 JS/TS 文件中提取 base64 常量
|
|
7
|
+
*
|
|
8
|
+
* 使用方法:
|
|
9
|
+
* node scripts/base64-to-mp3.js --input=<base64字符串或文件路径> --output=<输出mp3路径>
|
|
10
|
+
* node scripts/base64-to-mp3.js --file=./src/config/const.ts --var=Btn_Click_Audio --output=./output.mp3
|
|
11
|
+
* node scripts/base64-to-mp3.js --help
|
|
12
|
+
*
|
|
13
|
+
* 示例:
|
|
14
|
+
* # 从 JS 文件提取变量
|
|
15
|
+
* node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3
|
|
16
|
+
*
|
|
17
|
+
* # 直接传入 base64 字符串(适合短字符串)
|
|
18
|
+
* node scripts/base64-to-mp3.js --input="SGVsbG8..." -o output.mp3
|
|
19
|
+
*
|
|
20
|
+
* # 从文本文件读取 base64(适合长字符串)
|
|
21
|
+
* node scripts/base64-to-mp3.js --input=./base64.txt -o output.mp3
|
|
22
|
+
*
|
|
23
|
+
* # 支持 brotli 压缩的 base64(先解压再写入)
|
|
24
|
+
* node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3 --brotli
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 解析命令行参数
|
|
32
|
+
*/
|
|
33
|
+
function parseArgs() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const config = {
|
|
36
|
+
input: null, // base64 字符串或包含 base64 的文件路径
|
|
37
|
+
file: null, // JS/TS 源文件路径(从中提取变量)
|
|
38
|
+
varName: null, // 要提取的变量名
|
|
39
|
+
output: null, // 输出 mp3 文件路径
|
|
40
|
+
brotli: false, // 是否先进行 brotli 解压
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (arg === '--help' || arg === '-h') {
|
|
46
|
+
showHelp();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} else if (arg.startsWith('--input=')) {
|
|
49
|
+
config.input = arg.split('=').slice(1).join('=');
|
|
50
|
+
} else if (arg === '--input' || arg === '-i') {
|
|
51
|
+
config.input = args[++i];
|
|
52
|
+
} else if (arg.startsWith('--file=')) {
|
|
53
|
+
config.file = arg.split('=').slice(1).join('=');
|
|
54
|
+
} else if (arg.startsWith('--var=')) {
|
|
55
|
+
config.varName = arg.split('=').slice(1).join('=');
|
|
56
|
+
} else if (arg.startsWith('--output=') || arg.startsWith('-o=')) {
|
|
57
|
+
config.output = arg.split('=').slice(1).join('=');
|
|
58
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
59
|
+
config.output = args[++i];
|
|
60
|
+
} else if (arg === '--brotli') {
|
|
61
|
+
config.brotli = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 从 JS/TS 文件中提取指定变量的字符串值
|
|
70
|
+
*/
|
|
71
|
+
function extractVarFromFile(filePath, varName) {
|
|
72
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
73
|
+
|
|
74
|
+
// 匹配多种导出/赋值格式:
|
|
75
|
+
// export const Xxx = '...'
|
|
76
|
+
// export const Xxx = "..."
|
|
77
|
+
// const Xxx = '...'
|
|
78
|
+
// const Xxx = "..."
|
|
79
|
+
// export const Xxx = `...`
|
|
80
|
+
const patterns = [
|
|
81
|
+
// 单引号
|
|
82
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*'([^']*)'`, 's'),
|
|
83
|
+
// 双引号
|
|
84
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*"([^"]*)"`, 's'),
|
|
85
|
+
// 模板字符串
|
|
86
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*\`([^\`]*)\``, 's'),
|
|
87
|
+
// 带类型注解: const Xxx: string = '...'
|
|
88
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*:\\s*\\w+\\s*=\\s*'([^']*)'`, 's'),
|
|
89
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*:\\s*\\w+\\s*=\\s*"([^"]*)"`, 's'),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const pattern of patterns) {
|
|
93
|
+
const match = content.match(pattern);
|
|
94
|
+
if (match) {
|
|
95
|
+
return match[1];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取 base64 字符串
|
|
104
|
+
*/
|
|
105
|
+
function getBase64String(config) {
|
|
106
|
+
// 方式 1:从 JS/TS 文件提取变量
|
|
107
|
+
if (config.file && config.varName) {
|
|
108
|
+
const absPath = path.resolve(config.file);
|
|
109
|
+
if (!fs.existsSync(absPath)) {
|
|
110
|
+
console.error(`❌ File not found: ${absPath}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log(`📄 Reading variable "${config.varName}" from: ${absPath}`);
|
|
114
|
+
const value = extractVarFromFile(absPath, config.varName);
|
|
115
|
+
if (!value) {
|
|
116
|
+
console.error(`❌ Variable "${config.varName}" not found in ${config.file}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.log(`✅ Found variable, length: ${value.length} chars`);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 方式 2:直接输入或从文件读取
|
|
124
|
+
if (config.input) {
|
|
125
|
+
// 判断是文件路径还是 base64 字符串
|
|
126
|
+
if (fs.existsSync(config.input)) {
|
|
127
|
+
console.log(`📄 Reading base64 from file: ${config.input}`);
|
|
128
|
+
return fs.readFileSync(config.input, 'utf-8').trim();
|
|
129
|
+
}
|
|
130
|
+
// 直接当作 base64 字符串
|
|
131
|
+
return config.input;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.error('❌ No input specified. Use --input or --file + --var');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 格式化文件大小
|
|
140
|
+
*/
|
|
141
|
+
function formatSize(bytes) {
|
|
142
|
+
if (bytes === 0) return '0 B';
|
|
143
|
+
const k = 1024;
|
|
144
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
145
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
146
|
+
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 显示帮助
|
|
151
|
+
*/
|
|
152
|
+
function showHelp() {
|
|
153
|
+
console.log(`
|
|
154
|
+
🎵 Base64 音频转 MP3 文件工具
|
|
155
|
+
|
|
156
|
+
使用方法:
|
|
157
|
+
node scripts/base64-to-mp3.js [选项]
|
|
158
|
+
|
|
159
|
+
选项:
|
|
160
|
+
--file=<路径> JS/TS 源文件路径
|
|
161
|
+
--var=<变量名> 要从文件中提取的变量名
|
|
162
|
+
--input=<base64> 直接传入 base64 字符串,或包含 base64 的文本文件路径
|
|
163
|
+
--output=<路径>, -o 输出 MP3 文件路径(默认: ./output.mp3)
|
|
164
|
+
--brotli 先进行 brotli 解压(适用于压缩后的 base64)
|
|
165
|
+
--help, -h 显示帮助信息
|
|
166
|
+
|
|
167
|
+
示例:
|
|
168
|
+
# 从 const.ts 提取 Btn_Click_Audio 变量,转为 mp3
|
|
169
|
+
node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3
|
|
170
|
+
|
|
171
|
+
# brotli 压缩的 base64(先解压再写入)
|
|
172
|
+
node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3 --brotli
|
|
173
|
+
|
|
174
|
+
# base64 编码的 URL(自动识别并下载,如爱给网等素材站)
|
|
175
|
+
node scripts/base64-to-mp3.js --input="aHR0cHM6Ly9zMi5haWdlaS5jb20v..." -o bgm.mp3
|
|
176
|
+
|
|
177
|
+
# 从文本文件读取 base64
|
|
178
|
+
node scripts/base64-to-mp3.js --input=./audio_base64.txt -o output.mp3
|
|
179
|
+
|
|
180
|
+
# 直接传入短 base64 音频数据
|
|
181
|
+
node scripts/base64-to-mp3.js --input="AAAAIGZ0eXBpc29..." -o output.mp3
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 主函数
|
|
187
|
+
*/
|
|
188
|
+
async function main() {
|
|
189
|
+
const config = parseArgs();
|
|
190
|
+
|
|
191
|
+
if (!config.output) {
|
|
192
|
+
config.output = './output.mp3';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('='.repeat(50));
|
|
196
|
+
console.log('🎵 Base64 → MP3 转换工具');
|
|
197
|
+
console.log('='.repeat(50));
|
|
198
|
+
|
|
199
|
+
// 获取 base64 字符串
|
|
200
|
+
const base64String = getBase64String(config);
|
|
201
|
+
|
|
202
|
+
// 去除可能的 data URI 前缀
|
|
203
|
+
let cleanBase64 = base64String;
|
|
204
|
+
const dataUriMatch = base64String.match(/^data:[^;]+;base64,(.+)/);
|
|
205
|
+
if (dataUriMatch) {
|
|
206
|
+
cleanBase64 = dataUriMatch[1];
|
|
207
|
+
console.log('📎 Stripped data URI prefix');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 解码 base64
|
|
211
|
+
console.log(`🔄 Decoding base64 (${cleanBase64.length} chars)...`);
|
|
212
|
+
const buffer = Buffer.from(cleanBase64, 'base64');
|
|
213
|
+
const decodedStr = buffer.toString('utf-8');
|
|
214
|
+
|
|
215
|
+
// 自动识别:解码后是 URL 还是二进制音频数据
|
|
216
|
+
if (decodedStr.startsWith('http://') || decodedStr.startsWith('https://')) {
|
|
217
|
+
// base64 编码的是 URL,自动下载
|
|
218
|
+
const url = decodedStr.trim();
|
|
219
|
+
console.log(`🔗 Detected URL inside base64:`);
|
|
220
|
+
console.log(` ${url}`);
|
|
221
|
+
console.log(`🌐 Downloading MP3...`);
|
|
222
|
+
|
|
223
|
+
await downloadFile(url, config.output);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 二进制音频数据
|
|
228
|
+
console.log(` Decoded size: ${formatSize(buffer.length)}`);
|
|
229
|
+
let outputBuffer = buffer;
|
|
230
|
+
|
|
231
|
+
// brotli 解压(可选)
|
|
232
|
+
if (config.brotli) {
|
|
233
|
+
console.log('🗜️ Decompressing with brotli...');
|
|
234
|
+
try {
|
|
235
|
+
const brotli = require('brotli/decompress');
|
|
236
|
+
const decompressed = brotli(new Uint8Array(buffer));
|
|
237
|
+
if (!decompressed) {
|
|
238
|
+
console.error('❌ Brotli decompression returned null');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
outputBuffer = Buffer.from(decompressed);
|
|
242
|
+
console.log(` Decompressed size: ${formatSize(outputBuffer.length)}`);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error(`❌ Brotli decompression failed: ${e.message}`);
|
|
245
|
+
console.log(' Tip: npm install brotli');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 写入文件
|
|
251
|
+
saveFile(config.output, outputBuffer);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 下载文件
|
|
255
|
+
*/
|
|
256
|
+
function downloadFile(url, outputFilePath) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const outputPath = path.resolve(outputFilePath);
|
|
259
|
+
const outputDir = path.dirname(outputPath);
|
|
260
|
+
if (!fs.existsSync(outputDir)) {
|
|
261
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 根据协议选择 http 或 https
|
|
265
|
+
const httpModule = url.startsWith('https') ? require('https') : require('http');
|
|
266
|
+
|
|
267
|
+
const request = httpModule.get(url, (response) => {
|
|
268
|
+
// 处理重定向
|
|
269
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
270
|
+
console.log(` ↪ Redirecting to: ${response.headers.location}`);
|
|
271
|
+
return downloadFile(response.headers.location, outputFilePath).then(resolve).catch(reject);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (response.statusCode !== 200) {
|
|
275
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const contentLength = response.headers['content-length'];
|
|
280
|
+
if (contentLength) {
|
|
281
|
+
console.log(` Content-Length: ${formatSize(parseInt(contentLength))}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const fileStream = fs.createWriteStream(outputPath);
|
|
285
|
+
let downloadedBytes = 0;
|
|
286
|
+
|
|
287
|
+
response.on('data', (chunk) => {
|
|
288
|
+
downloadedBytes += chunk.length;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
response.pipe(fileStream);
|
|
292
|
+
|
|
293
|
+
fileStream.on('finish', () => {
|
|
294
|
+
fileStream.close();
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('='.repeat(50));
|
|
297
|
+
console.log(`✅ Downloaded to: ${outputPath}`);
|
|
298
|
+
console.log(`📦 File size: ${formatSize(downloadedBytes)}`);
|
|
299
|
+
console.log('='.repeat(50));
|
|
300
|
+
resolve();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
fileStream.on('error', (err) => {
|
|
304
|
+
fs.unlinkSync(outputPath);
|
|
305
|
+
reject(err);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
request.on('error', reject);
|
|
310
|
+
request.setTimeout(30000, () => {
|
|
311
|
+
request.destroy();
|
|
312
|
+
reject(new Error('Download timeout (30s)'));
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 保存 buffer 到文件
|
|
319
|
+
*/
|
|
320
|
+
function saveFile(outputFilePath, buffer) {
|
|
321
|
+
const outputPath = path.resolve(outputFilePath);
|
|
322
|
+
const outputDir = path.dirname(outputPath);
|
|
323
|
+
if (!fs.existsSync(outputDir)) {
|
|
324
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fs.writeFileSync(outputPath, buffer);
|
|
328
|
+
|
|
329
|
+
console.log('');
|
|
330
|
+
console.log('='.repeat(50));
|
|
331
|
+
console.log(`✅ Saved to: ${outputPath}`);
|
|
332
|
+
console.log(`📦 File size: ${formatSize(buffer.length)}`);
|
|
333
|
+
console.log('='.repeat(50));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 运行
|
|
337
|
+
main().catch(error => {
|
|
338
|
+
console.error('❌ Error:', error.message);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
});
|