melody-harvest 1.0.0
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 +97 -0
- package/babel.config.json +9 -0
- package/bin/melody-harvest.js +207 -0
- package/dist/downloader.js +293 -0
- package/dist/index.js +9 -0
- package/package.json +54 -0
- package/src/downloader.js +338 -0
- package/src/index.js +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# MelodyHarvest
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/melody-harvest)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
|
6
|
+
[]()
|
|
7
|
+
|
|
8
|
+
**🎵 一个强大的全平台音乐搜索与下载CLI工具,让你的音乐收藏无处不在!**
|
|
9
|
+
|
|
10
|
+
> 🌟 从四大主流音乐平台一键搜索和下载高品质音乐,构建属于你的个人音乐图书馆
|
|
11
|
+
|
|
12
|
+
## ✨ 核心特性
|
|
13
|
+
|
|
14
|
+
### 🎯 全平台支持
|
|
15
|
+
- **咪咕音乐** - 海量正版音乐资源
|
|
16
|
+
- **酷狗音乐** - 热门流行歌曲全覆盖
|
|
17
|
+
- **QQ音乐** - 独家版权歌曲获取
|
|
18
|
+
- **网易云音乐** - 高品质音乐与独立音乐人作品
|
|
19
|
+
|
|
20
|
+
### 🚀 智能搜索
|
|
21
|
+
- **精准搜索** - 按歌曲名、歌手名快速定位
|
|
22
|
+
- **全网搜索** - 一次性搜索所有平台,对比选择
|
|
23
|
+
- **批量下载** - 支持多首歌曲同时下载
|
|
24
|
+
- **历史记录** - 完整的下载日志管理
|
|
25
|
+
|
|
26
|
+
### 💫 精美界面
|
|
27
|
+
- **绚丽的CLI界面** - 渐变色标题、动态加载动画
|
|
28
|
+
- **进度条显示** - 实时下载进度和速度
|
|
29
|
+
- **交互式操作** - 简单直观的命令行交互
|
|
30
|
+
|
|
31
|
+
## 📦 安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 使用 npm 全局安装
|
|
35
|
+
npm install -g melody-harvest
|
|
36
|
+
|
|
37
|
+
# 或使用 yarn
|
|
38
|
+
yarn global add melody-harvest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 🎮 使用方法
|
|
42
|
+
|
|
43
|
+
### 启动程序
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
melody-harvest
|
|
47
|
+
# 或
|
|
48
|
+
npx melody-harvest
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 基本操作
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
🔍 输入搜索关键词 (如: 周杰伦 晴天)
|
|
55
|
+
🌐 选择搜索平台 (咪咕/酷狗/QQ/网易云/全网)
|
|
56
|
+
📋 选择要下载的歌曲
|
|
57
|
+
⬇️ 确认下载
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 功能说明
|
|
61
|
+
|
|
62
|
+
- **搜索歌曲**: 直接输入歌曲名或歌手名
|
|
63
|
+
- **平台切换**: 选择不同音乐平台搜索
|
|
64
|
+
- **批量下载**: 输入多个序号同时下载多首歌曲
|
|
65
|
+
- **查看历史**: 查看已下载的歌曲记录
|
|
66
|
+
|
|
67
|
+
## 🔧 命令行参数
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
melody-harvest --help # 显示帮助信息
|
|
71
|
+
melody-harvest --version # 显示版本号
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 📂 输出目录
|
|
75
|
+
|
|
76
|
+
默认下载目录: `./downloads`
|
|
77
|
+
|
|
78
|
+
## 🐛 故障排除
|
|
79
|
+
|
|
80
|
+
**常见问题解决方案:**
|
|
81
|
+
|
|
82
|
+
- 🔄 **下载失败**:检查网络连接或尝试其他平台
|
|
83
|
+
- 🔤 **文件名问题**:程序会自动处理特殊字符
|
|
84
|
+
- 🔍 **搜索无结果**:尝试使用更精确的关键词
|
|
85
|
+
- ⏱️ **连接超时**:网络环境不佳,可重试
|
|
86
|
+
|
|
87
|
+
## 🤝 贡献
|
|
88
|
+
|
|
89
|
+
欢迎提交 Issue 和 Pull Request!
|
|
90
|
+
|
|
91
|
+
## 📄 许可证
|
|
92
|
+
|
|
93
|
+
本项目采用 GPL-2.0 许可证开源。
|
|
94
|
+
|
|
95
|
+
## 🙏 致谢
|
|
96
|
+
|
|
97
|
+
感谢所有为开源社区做出贡献的人们!
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const figlet = require('figlet');
|
|
5
|
+
const gradient = require('gradient-string');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const { MusicDownloader } = require('../src/downloader');
|
|
8
|
+
const { createInterface } = require('readline');
|
|
9
|
+
|
|
10
|
+
const musicDownloader = new MusicDownloader();
|
|
11
|
+
|
|
12
|
+
async function showWelcome() {
|
|
13
|
+
console.clear();
|
|
14
|
+
console.log(
|
|
15
|
+
gradient.teen(
|
|
16
|
+
figlet.textSync('MelodyHarvest', {
|
|
17
|
+
font: 'Big',
|
|
18
|
+
horizontalLayout: 'default',
|
|
19
|
+
verticalLayout: 'default'
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
console.log(chalk.cyan('\n 🎵 全平台音乐搜索与下载利器'));
|
|
24
|
+
console.log(chalk.gray(' =====================================\n'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function searchMusic(query, platform = null) {
|
|
28
|
+
const platforms = {
|
|
29
|
+
'migu': '咪咕音乐',
|
|
30
|
+
'kugou': '酷狗音乐',
|
|
31
|
+
'qq': 'QQ音乐',
|
|
32
|
+
'cloud': '网易云',
|
|
33
|
+
'all': '全网搜索'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (platform && platform !== 'all') {
|
|
37
|
+
return await musicDownloader.search(query, platform);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = await musicDownloader.searchAll(query);
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function downloadMusic(songs, platform) {
|
|
45
|
+
for (const song of songs) {
|
|
46
|
+
await musicDownloader.download(song, platform);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function showHistory() {
|
|
51
|
+
const history = musicDownloader.getHistory();
|
|
52
|
+
if (history.length === 0) {
|
|
53
|
+
console.log(chalk.yellow('\n📭 暂无下载记录'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.cyan('\n📜 下载历史:\n'));
|
|
58
|
+
history.forEach((item, index) => {
|
|
59
|
+
console.log(
|
|
60
|
+
chalk.white(`${index + 1}. `) +
|
|
61
|
+
chalk.green(item.title) +
|
|
62
|
+
chalk.gray(' - ') +
|
|
63
|
+
chalk.yellow(item.artist) +
|
|
64
|
+
chalk.gray(' [') +
|
|
65
|
+
chalk.blue(item.platform) +
|
|
66
|
+
chalk.gray(']')
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
await showWelcome();
|
|
73
|
+
|
|
74
|
+
const questions = [
|
|
75
|
+
{
|
|
76
|
+
type: 'input',
|
|
77
|
+
name: 'search',
|
|
78
|
+
message: chalk.cyan('🔍 请输入搜索关键词 (如: 周杰伦 晴天)'),
|
|
79
|
+
validate: (input) => input.trim() !== '' || '请输入搜索内容'
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const answers = await inquirer.prompt(questions);
|
|
84
|
+
const query = answers.search.trim();
|
|
85
|
+
|
|
86
|
+
const platformQuestions = [
|
|
87
|
+
{
|
|
88
|
+
type: 'list',
|
|
89
|
+
name: 'platform',
|
|
90
|
+
message: chalk.cyan('🌐 选择搜索平台'),
|
|
91
|
+
choices: [
|
|
92
|
+
{ name: '🎵 咪咕音乐', value: 'migu' },
|
|
93
|
+
{ name: '🎤 酷狗音乐', value: 'kugou' },
|
|
94
|
+
{ name: '🎼 QQ音乐', value: 'qq' },
|
|
95
|
+
{ name: '☁️ 网易云音乐', value: 'cloud' },
|
|
96
|
+
{ name: '🌈 全网搜索 (所有平台)', value: 'all' },
|
|
97
|
+
{ name: '🔙 返回', value: 'back' }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const platformAnswer = await inquirer.prompt(platformQuestions);
|
|
103
|
+
|
|
104
|
+
if (platformAnswer.platform === 'back') {
|
|
105
|
+
return main();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const results = await searchMusic(query, platformAnswer.platform);
|
|
109
|
+
|
|
110
|
+
if (!results || results.length === 0) {
|
|
111
|
+
console.log(chalk.red('\n❌ 未找到相关歌曲,请尝试其他关键词'));
|
|
112
|
+
return main();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.cyan('\n📋 搜索结果:\n'));
|
|
116
|
+
results.forEach((song, index) => {
|
|
117
|
+
const platformEmoji = {
|
|
118
|
+
'migu': '🎵',
|
|
119
|
+
'kugou': '🎤',
|
|
120
|
+
'qq': '🎼',
|
|
121
|
+
'cloud': '☁️'
|
|
122
|
+
};
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.white(`${index.toString().padStart(2, '0')}. `) +
|
|
125
|
+
chalk.green(song.title) +
|
|
126
|
+
chalk.gray(' - ') +
|
|
127
|
+
chalk.yellow(song.artist) +
|
|
128
|
+
chalk.gray(' [') +
|
|
129
|
+
chalk.blue(song.album || '未知专辑') +
|
|
130
|
+
chalk.gray(']')
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const downloadQuestions = [
|
|
135
|
+
{
|
|
136
|
+
type: 'input',
|
|
137
|
+
name: 'indices',
|
|
138
|
+
message: chalk.cyan('⬇️ 输入要下载的歌曲序号 (多个用空格分隔,如: 1 3 5)'),
|
|
139
|
+
validate: (input) => {
|
|
140
|
+
if (!input.trim()) return '请输入序号';
|
|
141
|
+
const indices = input.trim().split(/\s+/).map(i => parseInt(i));
|
|
142
|
+
if (indices.some(i => isNaN(i) || i < 1 || i > results.length)) {
|
|
143
|
+
return `请输入 1-${results.length} 之间的有效序号`;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'confirm',
|
|
150
|
+
name: 'confirmDownload',
|
|
151
|
+
message: chalk.cyan('✨ 确定开始下载吗?'),
|
|
152
|
+
default: true
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const downloadAnswers = await inquirer.prompt(downloadQuestions);
|
|
157
|
+
|
|
158
|
+
if (!downloadAnswers.confirmDownload) {
|
|
159
|
+
console.log(chalk.gray('\n⏸️ 已取消下载'));
|
|
160
|
+
return main();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const indices = downloadAnswers.indices.trim().split(/\s+/).map(i => parseInt(i) - 1);
|
|
164
|
+
const songsToDownload = indices.map(i => results[i]).filter(song => song);
|
|
165
|
+
|
|
166
|
+
console.log(chalk.cyan('\n🚀 开始下载...\n'));
|
|
167
|
+
await downloadMusic(songsToDownload, platformAnswer.platform);
|
|
168
|
+
|
|
169
|
+
console.log(chalk.green('\n✅ 下载完成!'));
|
|
170
|
+
|
|
171
|
+
const continueQuestions = [
|
|
172
|
+
{
|
|
173
|
+
type: 'list',
|
|
174
|
+
name: 'action',
|
|
175
|
+
message: chalk.cyan('\n🎮 选择操作'),
|
|
176
|
+
choices: [
|
|
177
|
+
{ name: '🔍 继续搜索', value: 'search' },
|
|
178
|
+
{ name: '📜 查看历史', value: 'history' },
|
|
179
|
+
{ name: '👋 退出程序', value: 'exit' }
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const actionAnswer = await inquirer.prompt(continueQuestions);
|
|
185
|
+
|
|
186
|
+
switch (actionAnswer.action) {
|
|
187
|
+
case 'search':
|
|
188
|
+
return main();
|
|
189
|
+
case 'history':
|
|
190
|
+
await showHistory();
|
|
191
|
+
return main();
|
|
192
|
+
case 'exit':
|
|
193
|
+
console.log(chalk.cyan('\n👋 感谢使用 MelodyHarvest,再见!\n'));
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.on('uncaughtException', (error) => {
|
|
199
|
+
console.error(chalk.red('\n❌ 程序发生错误:'), error.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
204
|
+
console.error(chalk.red('\n❌ 未处理的Promise错误:'), reason);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
main();
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
promisify
|
|
8
|
+
} = require('util');
|
|
9
|
+
const figlet = require('figlet');
|
|
10
|
+
const gradient = require('gradient-string');
|
|
11
|
+
const ora = require('ora');
|
|
12
|
+
const cliProgress = require('cli-progress');
|
|
13
|
+
const writeFileAsync = promisify(fs.writeFile);
|
|
14
|
+
const mkdirAsync = promisify(fs.mkdir);
|
|
15
|
+
class MusicDownloader {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.history = [];
|
|
18
|
+
this.downloadPath = path.join(process.cwd(), 'downloads');
|
|
19
|
+
this.ensureDownloadDir();
|
|
20
|
+
}
|
|
21
|
+
async ensureDownloadDir() {
|
|
22
|
+
try {
|
|
23
|
+
await mkdirAsync(this.downloadPath, {
|
|
24
|
+
recursive: true
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code !== 'EEXIST') {
|
|
28
|
+
console.error('创建下载目录失败:', error.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async search(query, platform = 'all') {
|
|
33
|
+
const platformMap = {
|
|
34
|
+
'migu': () => this.searchMigu(query),
|
|
35
|
+
'kugou': () => this.searchKugou(query),
|
|
36
|
+
'qq': () => this.searchQQ(query),
|
|
37
|
+
'cloud': () => this.searchNetease(query),
|
|
38
|
+
'all': () => this.searchAll(query)
|
|
39
|
+
};
|
|
40
|
+
const searchFn = platformMap[platform];
|
|
41
|
+
if (!searchFn) {
|
|
42
|
+
throw new Error(`不支持的平台: ${platform}`);
|
|
43
|
+
}
|
|
44
|
+
return await searchFn();
|
|
45
|
+
}
|
|
46
|
+
async searchMigu(query) {
|
|
47
|
+
const spinner = ora({
|
|
48
|
+
text: '🎵 正在搜索咪咕音乐...',
|
|
49
|
+
spinner: 'dots'
|
|
50
|
+
}).start();
|
|
51
|
+
try {
|
|
52
|
+
const response = await axios.get('https://music.migu.cn/v3/api/search/search', {
|
|
53
|
+
params: {
|
|
54
|
+
keyword: query,
|
|
55
|
+
page: 1,
|
|
56
|
+
limit: 20
|
|
57
|
+
},
|
|
58
|
+
timeout: 10000
|
|
59
|
+
});
|
|
60
|
+
spinner.succeed('咪咕音乐搜索完成');
|
|
61
|
+
if (response.data && response.data.data) {
|
|
62
|
+
return response.data.data.map(song => ({
|
|
63
|
+
id: song.id,
|
|
64
|
+
title: song.title || song.songName,
|
|
65
|
+
artist: song.artist || song.singerName,
|
|
66
|
+
album: song.album || song.albumName,
|
|
67
|
+
duration: song.duration || song.time,
|
|
68
|
+
url: song.url || song.playUrl,
|
|
69
|
+
platform: '咪咕音乐',
|
|
70
|
+
platformCode: 'migu'
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
} catch (error) {
|
|
75
|
+
spinner.fail('咪咕音乐搜索失败');
|
|
76
|
+
console.error('搜索错误:', error.message);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async searchKugou(query) {
|
|
81
|
+
const spinner = ora({
|
|
82
|
+
text: '🎤 正在搜索酷狗音乐...',
|
|
83
|
+
spinner: 'dots'
|
|
84
|
+
}).start();
|
|
85
|
+
try {
|
|
86
|
+
const response = await axios.get('https://searchproxy.kugou.com/search', {
|
|
87
|
+
params: {
|
|
88
|
+
keyword: query,
|
|
89
|
+
page: 1,
|
|
90
|
+
limit: 20,
|
|
91
|
+
client: 'mse'
|
|
92
|
+
},
|
|
93
|
+
timeout: 10000,
|
|
94
|
+
headers: {
|
|
95
|
+
'Referer': 'https://www.kugou.com/'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
spinner.succeed('酷狗音乐搜索完成');
|
|
99
|
+
if (response.data && response.data.data) {
|
|
100
|
+
return response.data.data.map(song => ({
|
|
101
|
+
id: song.FileHash || song.id,
|
|
102
|
+
title: song.SongName || song.fileName,
|
|
103
|
+
artist: song.SingerName || song.singer,
|
|
104
|
+
album: song.AlbumName || song.album,
|
|
105
|
+
duration: song.Duration || song.time,
|
|
106
|
+
url: song.FileHash ? `https://www.kugou.com/song/#${song.FileHash}` : song.play_url,
|
|
107
|
+
platform: '酷狗音乐',
|
|
108
|
+
platformCode: 'kugou'
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
} catch (error) {
|
|
113
|
+
spinner.fail('酷狗音乐搜索失败');
|
|
114
|
+
console.error('搜索错误:', error.message);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async searchQQ(query) {
|
|
119
|
+
const spinner = ora({
|
|
120
|
+
text: '🎼 正在搜索QQ音乐...',
|
|
121
|
+
spinner: 'dots'
|
|
122
|
+
}).start();
|
|
123
|
+
try {
|
|
124
|
+
const response = await axios.get('https://c.y.qq.com/soso/fcgi-bin/client_search_cp', {
|
|
125
|
+
params: {
|
|
126
|
+
ct: 24,
|
|
127
|
+
qqmusic_ver: 1001,
|
|
128
|
+
remoteplace: 'txt.yqq.top',
|
|
129
|
+
searchid: Math.floor(Math.random() * 1000000),
|
|
130
|
+
t: 0,
|
|
131
|
+
flag: 1,
|
|
132
|
+
sem: 1,
|
|
133
|
+
aggr: 1,
|
|
134
|
+
n: 20,
|
|
135
|
+
w: query,
|
|
136
|
+
lr: 0,
|
|
137
|
+
lossless: 1
|
|
138
|
+
},
|
|
139
|
+
timeout: 10000,
|
|
140
|
+
headers: {
|
|
141
|
+
'Referer': 'https://y.qq.com/'
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
spinner.succeed('QQ音乐搜索完成');
|
|
145
|
+
if (response.data && response.data.message) {
|
|
146
|
+
const data = JSON.parse(response.data.message.data);
|
|
147
|
+
if (data.song && data.song.list) {
|
|
148
|
+
return data.song.list.map(song => ({
|
|
149
|
+
id: song.songmid,
|
|
150
|
+
title: song.songname,
|
|
151
|
+
artist: song.singer[0] ? song.singer[0].name : '未知',
|
|
152
|
+
album: song.albumname,
|
|
153
|
+
duration: song.interval,
|
|
154
|
+
url: `https://y.qq.com/#/song/${song.songmid}`,
|
|
155
|
+
platform: 'QQ音乐',
|
|
156
|
+
platformCode: 'qq'
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
} catch (error) {
|
|
162
|
+
spinner.fail('QQ音乐搜索失败');
|
|
163
|
+
console.error('搜索错误:', error.message);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async searchNetease(query) {
|
|
168
|
+
const spinner = ora({
|
|
169
|
+
text: '☁️ 正在搜索网易云音乐...',
|
|
170
|
+
spinner: 'dots'
|
|
171
|
+
}).start();
|
|
172
|
+
try {
|
|
173
|
+
const response = await axios.post('https://music.163.com/api/search/get/web', {
|
|
174
|
+
s: query,
|
|
175
|
+
type: 1,
|
|
176
|
+
limit: 20,
|
|
177
|
+
offset: 0
|
|
178
|
+
}, {
|
|
179
|
+
timeout: 10000,
|
|
180
|
+
headers: {
|
|
181
|
+
'Referer': 'https://music.163.com/',
|
|
182
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
spinner.succeed('网易云音乐搜索完成');
|
|
186
|
+
if (response.data && response.data.result && response.data.result.songs) {
|
|
187
|
+
return response.data.result.songs.map(song => ({
|
|
188
|
+
id: song.id,
|
|
189
|
+
title: song.name,
|
|
190
|
+
artist: song.artists[0] ? song.artists[0].name : '未知',
|
|
191
|
+
album: song.album ? song.album.name : '未知专辑',
|
|
192
|
+
duration: song.duration,
|
|
193
|
+
url: `https://music.163.com/#/song?id=${song.id}`,
|
|
194
|
+
platform: '网易云音乐',
|
|
195
|
+
platformCode: 'cloud'
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
return [];
|
|
199
|
+
} catch (error) {
|
|
200
|
+
spinner.fail('网易云音乐搜索失败');
|
|
201
|
+
console.error('搜索错误:', error.message);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async searchAll(query) {
|
|
206
|
+
const spinner = ora({
|
|
207
|
+
text: '🌈 正在全网搜索...',
|
|
208
|
+
spinner: 'dots'
|
|
209
|
+
}).start();
|
|
210
|
+
try {
|
|
211
|
+
const [migu, kugou, qq, cloud] = await Promise.allSettled([this.searchMigu(query), this.searchKugou(query), this.searchQQ(query), this.searchNetease(query)]);
|
|
212
|
+
const results = [];
|
|
213
|
+
if (migu.status === 'fulfilled') results.push(...migu.value);
|
|
214
|
+
if (kugou.status === 'fulfilled') results.push(...kugou.value);
|
|
215
|
+
if (qq.status === 'fulfilled') results.push(...qq.value);
|
|
216
|
+
if (cloud.status === 'fulfilled') results.push(...cloud.value);
|
|
217
|
+
spinner.succeed(`全网搜索完成,共找到 ${results.length} 首歌曲`);
|
|
218
|
+
return results;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
spinner.fail('全网搜索失败');
|
|
221
|
+
console.error('搜索错误:', error.message);
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async download(song, platform) {
|
|
226
|
+
const fileName = this.sanitizeFileName(`${song.artist} - ${song.title}`);
|
|
227
|
+
const filePath = path.join(this.downloadPath, `${fileName}.mp3`);
|
|
228
|
+
const progressBar = new cliProgress.SingleBar({
|
|
229
|
+
format: `⬇️ ${chalk.cyan(fileName)} | ${chalk.green('{bar}')} {percentage}% | {speed}`,
|
|
230
|
+
barCompleteChar: '█',
|
|
231
|
+
barIncompleteChar: '░',
|
|
232
|
+
fps: 10,
|
|
233
|
+
stream: process.stdout,
|
|
234
|
+
position: 'center',
|
|
235
|
+
schedule: function (schedule) {
|
|
236
|
+
return schedule;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
progressBar.start(100, 0);
|
|
240
|
+
try {
|
|
241
|
+
const response = await axios({
|
|
242
|
+
method: 'get',
|
|
243
|
+
url: song.url,
|
|
244
|
+
responseType: 'stream',
|
|
245
|
+
timeout: 60000,
|
|
246
|
+
onDownloadProgress: progressEvent => {
|
|
247
|
+
const percent = Math.round(progressEvent.loaded * 100 / (progressEvent.total || progressEvent.loaded));
|
|
248
|
+
progressBar.update(percent);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const totalLength = parseInt(response.headers['content-length'] || '0', 10);
|
|
252
|
+
const writer = fs.createWriteStream(filePath);
|
|
253
|
+
response.data.on('data', chunk => {
|
|
254
|
+
progressBar.update(Math.min(99, Math.round(writer.bytesWritten * 100 / (totalLength || writer.bytesWritten))));
|
|
255
|
+
});
|
|
256
|
+
response.data.pipe(writer);
|
|
257
|
+
await new Promise((resolve, reject) => {
|
|
258
|
+
writer.on('finish', resolve);
|
|
259
|
+
writer.on('error', reject);
|
|
260
|
+
});
|
|
261
|
+
progressBar.stop();
|
|
262
|
+
this.history.push({
|
|
263
|
+
title: song.title,
|
|
264
|
+
artist: song.artist,
|
|
265
|
+
album: song.album,
|
|
266
|
+
platform: song.platform,
|
|
267
|
+
filePath: filePath,
|
|
268
|
+
downloadDate: new Date().toISOString()
|
|
269
|
+
});
|
|
270
|
+
console.log(chalk.green(`\n✅ 下载成功: ${song.artist} - ${song.title}`));
|
|
271
|
+
console.log(chalk.gray(` 📁 保存至: ${filePath}`));
|
|
272
|
+
return filePath;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
progressBar.stop();
|
|
275
|
+
console.error(chalk.red(`\n❌ 下载失败: ${song.title}`));
|
|
276
|
+
console.error(chalk.gray(` 错误信息: ${error.message}`));
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
sanitizeFileName(fileName) {
|
|
281
|
+
return fileName.replace(/[<>:"/\\|?*]/g, '-').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
282
|
+
}
|
|
283
|
+
getHistory() {
|
|
284
|
+
return this.history;
|
|
285
|
+
}
|
|
286
|
+
clearHistory() {
|
|
287
|
+
this.history = [];
|
|
288
|
+
console.log(chalk.cyan('\n🗑️ 下载历史已清空'));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
module.exports = {
|
|
292
|
+
MusicDownloader
|
|
293
|
+
};
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "melody-harvest",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "🎵 一个强大的全平台音乐搜索与下载CLI工具 - 支持咪咕、酷狗、QQ音乐、网易云",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"melody-harvest": "./bin/melody-harvest.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "babel src --out-dir dist",
|
|
11
|
+
"dev": "node bin/melody-harvest.js",
|
|
12
|
+
"start": "node bin/melody-harvest.js",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"music",
|
|
17
|
+
"download",
|
|
18
|
+
"cli",
|
|
19
|
+
"migu",
|
|
20
|
+
"kugou",
|
|
21
|
+
"qq-music",
|
|
22
|
+
"netease",
|
|
23
|
+
"music-downloader"
|
|
24
|
+
],
|
|
25
|
+
"author": "MelodyHarvest",
|
|
26
|
+
"license": "GPL-2.0",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/httpspycharmhelpers/MelodyHarvest.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/httpspycharmhelpers/MelodyHarvest/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/httpspycharmhelpers/MelodyHarvest#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"axios": "^1.6.2",
|
|
40
|
+
"chalk": "^4.1.2",
|
|
41
|
+
"chalk-animation": "^2.0.3",
|
|
42
|
+
"cli-progress": "^3.12.0",
|
|
43
|
+
"cli-spinners": "^2.9.2",
|
|
44
|
+
"figlet": "^1.7.0",
|
|
45
|
+
"gradient-string": "^2.0.2",
|
|
46
|
+
"inquirer": "^9.2.12",
|
|
47
|
+
"ora": "^8.0.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@babel/cli": "^7.28.6",
|
|
51
|
+
"@babel/core": "^7.29.0",
|
|
52
|
+
"@babel/preset-env": "^7.29.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { promisify } = require('util');
|
|
5
|
+
const figlet = require('figlet');
|
|
6
|
+
const gradient = require('gradient-string');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const cliProgress = require('cli-progress');
|
|
9
|
+
|
|
10
|
+
const writeFileAsync = promisify(fs.writeFile);
|
|
11
|
+
const mkdirAsync = promisify(fs.mkdir);
|
|
12
|
+
|
|
13
|
+
class MusicDownloader {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.history = [];
|
|
16
|
+
this.downloadPath = path.join(process.cwd(), 'downloads');
|
|
17
|
+
this.ensureDownloadDir();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async ensureDownloadDir() {
|
|
21
|
+
try {
|
|
22
|
+
await mkdirAsync(this.downloadPath, { recursive: true });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code !== 'EEXIST') {
|
|
25
|
+
console.error('创建下载目录失败:', error.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async search(query, platform = 'all') {
|
|
31
|
+
const platformMap = {
|
|
32
|
+
'migu': () => this.searchMigu(query),
|
|
33
|
+
'kugou': () => this.searchKugou(query),
|
|
34
|
+
'qq': () => this.searchQQ(query),
|
|
35
|
+
'cloud': () => this.searchNetease(query),
|
|
36
|
+
'all': () => this.searchAll(query)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const searchFn = platformMap[platform];
|
|
40
|
+
if (!searchFn) {
|
|
41
|
+
throw new Error(`不支持的平台: ${platform}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await searchFn();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async searchMigu(query) {
|
|
48
|
+
const spinner = ora({
|
|
49
|
+
text: '🎵 正在搜索咪咕音乐...',
|
|
50
|
+
spinner: 'dots'
|
|
51
|
+
}).start();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await axios.get('https://music.migu.cn/v3/api/search/search', {
|
|
55
|
+
params: {
|
|
56
|
+
keyword: query,
|
|
57
|
+
page: 1,
|
|
58
|
+
limit: 20
|
|
59
|
+
},
|
|
60
|
+
timeout: 10000
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
spinner.succeed('咪咕音乐搜索完成');
|
|
64
|
+
|
|
65
|
+
if (response.data && response.data.data) {
|
|
66
|
+
return response.data.data.map(song => ({
|
|
67
|
+
id: song.id,
|
|
68
|
+
title: song.title || song.songName,
|
|
69
|
+
artist: song.artist || song.singerName,
|
|
70
|
+
album: song.album || song.albumName,
|
|
71
|
+
duration: song.duration || song.time,
|
|
72
|
+
url: song.url || song.playUrl,
|
|
73
|
+
platform: '咪咕音乐',
|
|
74
|
+
platformCode: 'migu'
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
} catch (error) {
|
|
79
|
+
spinner.fail('咪咕音乐搜索失败');
|
|
80
|
+
console.error('搜索错误:', error.message);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async searchKugou(query) {
|
|
86
|
+
const spinner = ora({
|
|
87
|
+
text: '🎤 正在搜索酷狗音乐...',
|
|
88
|
+
spinner: 'dots'
|
|
89
|
+
}).start();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const response = await axios.get('https://searchproxy.kugou.com/search', {
|
|
93
|
+
params: {
|
|
94
|
+
keyword: query,
|
|
95
|
+
page: 1,
|
|
96
|
+
limit: 20,
|
|
97
|
+
client: 'mse'
|
|
98
|
+
},
|
|
99
|
+
timeout: 10000,
|
|
100
|
+
headers: {
|
|
101
|
+
'Referer': 'https://www.kugou.com/'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
spinner.succeed('酷狗音乐搜索完成');
|
|
106
|
+
|
|
107
|
+
if (response.data && response.data.data) {
|
|
108
|
+
return response.data.data.map(song => ({
|
|
109
|
+
id: song.FileHash || song.id,
|
|
110
|
+
title: song.SongName || song.fileName,
|
|
111
|
+
artist: song.SingerName || song.singer,
|
|
112
|
+
album: song.AlbumName || song.album,
|
|
113
|
+
duration: song.Duration || song.time,
|
|
114
|
+
url: song.FileHash ? `https://www.kugou.com/song/#${song.FileHash}` : song.play_url,
|
|
115
|
+
platform: '酷狗音乐',
|
|
116
|
+
platformCode: 'kugou'
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
} catch (error) {
|
|
121
|
+
spinner.fail('酷狗音乐搜索失败');
|
|
122
|
+
console.error('搜索错误:', error.message);
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async searchQQ(query) {
|
|
128
|
+
const spinner = ora({
|
|
129
|
+
text: '🎼 正在搜索QQ音乐...',
|
|
130
|
+
spinner: 'dots'
|
|
131
|
+
}).start();
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const response = await axios.get('https://c.y.qq.com/soso/fcgi-bin/client_search_cp', {
|
|
135
|
+
params: {
|
|
136
|
+
ct: 24,
|
|
137
|
+
qqmusic_ver: 1001,
|
|
138
|
+
remoteplace: 'txt.yqq.top',
|
|
139
|
+
searchid: Math.floor(Math.random() * 1000000),
|
|
140
|
+
t: 0,
|
|
141
|
+
flag: 1,
|
|
142
|
+
sem: 1,
|
|
143
|
+
aggr: 1,
|
|
144
|
+
n: 20,
|
|
145
|
+
w: query,
|
|
146
|
+
lr: 0,
|
|
147
|
+
lossless: 1
|
|
148
|
+
},
|
|
149
|
+
timeout: 10000,
|
|
150
|
+
headers: {
|
|
151
|
+
'Referer': 'https://y.qq.com/'
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
spinner.succeed('QQ音乐搜索完成');
|
|
156
|
+
|
|
157
|
+
if (response.data && response.data.message) {
|
|
158
|
+
const data = JSON.parse(response.data.message.data);
|
|
159
|
+
if (data.song && data.song.list) {
|
|
160
|
+
return data.song.list.map(song => ({
|
|
161
|
+
id: song.songmid,
|
|
162
|
+
title: song.songname,
|
|
163
|
+
artist: song.singer[0] ? song.singer[0].name : '未知',
|
|
164
|
+
album: song.albumname,
|
|
165
|
+
duration: song.interval,
|
|
166
|
+
url: `https://y.qq.com/#/song/${song.songmid}`,
|
|
167
|
+
platform: 'QQ音乐',
|
|
168
|
+
platformCode: 'qq'
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
} catch (error) {
|
|
174
|
+
spinner.fail('QQ音乐搜索失败');
|
|
175
|
+
console.error('搜索错误:', error.message);
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async searchNetease(query) {
|
|
181
|
+
const spinner = ora({
|
|
182
|
+
text: '☁️ 正在搜索网易云音乐...',
|
|
183
|
+
spinner: 'dots'
|
|
184
|
+
}).start();
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response = await axios.post('https://music.163.com/api/search/get/web', {
|
|
188
|
+
s: query,
|
|
189
|
+
type: 1,
|
|
190
|
+
limit: 20,
|
|
191
|
+
offset: 0
|
|
192
|
+
}, {
|
|
193
|
+
timeout: 10000,
|
|
194
|
+
headers: {
|
|
195
|
+
'Referer': 'https://music.163.com/',
|
|
196
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
spinner.succeed('网易云音乐搜索完成');
|
|
201
|
+
|
|
202
|
+
if (response.data && response.data.result && response.data.result.songs) {
|
|
203
|
+
return response.data.result.songs.map(song => ({
|
|
204
|
+
id: song.id,
|
|
205
|
+
title: song.name,
|
|
206
|
+
artist: song.artists[0] ? song.artists[0].name : '未知',
|
|
207
|
+
album: song.album ? song.album.name : '未知专辑',
|
|
208
|
+
duration: song.duration,
|
|
209
|
+
url: `https://music.163.com/#/song?id=${song.id}`,
|
|
210
|
+
platform: '网易云音乐',
|
|
211
|
+
platformCode: 'cloud'
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
return [];
|
|
215
|
+
} catch (error) {
|
|
216
|
+
spinner.fail('网易云音乐搜索失败');
|
|
217
|
+
console.error('搜索错误:', error.message);
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async searchAll(query) {
|
|
223
|
+
const spinner = ora({
|
|
224
|
+
text: '🌈 正在全网搜索...',
|
|
225
|
+
spinner: 'dots'
|
|
226
|
+
}).start();
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const [migu, kugou, qq, cloud] = await Promise.allSettled([
|
|
230
|
+
this.searchMigu(query),
|
|
231
|
+
this.searchKugou(query),
|
|
232
|
+
this.searchQQ(query),
|
|
233
|
+
this.searchNetease(query)
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const results = [];
|
|
237
|
+
if (migu.status === 'fulfilled') results.push(...migu.value);
|
|
238
|
+
if (kugou.status === 'fulfilled') results.push(...kugou.value);
|
|
239
|
+
if (qq.status === 'fulfilled') results.push(...qq.value);
|
|
240
|
+
if (cloud.status === 'fulfilled') results.push(...cloud.value);
|
|
241
|
+
|
|
242
|
+
spinner.succeed(`全网搜索完成,共找到 ${results.length} 首歌曲`);
|
|
243
|
+
|
|
244
|
+
return results;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
spinner.fail('全网搜索失败');
|
|
247
|
+
console.error('搜索错误:', error.message);
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async download(song, platform) {
|
|
253
|
+
const fileName = this.sanitizeFileName(`${song.artist} - ${song.title}`);
|
|
254
|
+
const filePath = path.join(this.downloadPath, `${fileName}.mp3`);
|
|
255
|
+
|
|
256
|
+
const progressBar = new cliProgress.SingleBar({
|
|
257
|
+
format: `⬇️ ${chalk.cyan(fileName)} | ${chalk.green('{bar}')} {percentage}% | {speed}`,
|
|
258
|
+
barCompleteChar: '█',
|
|
259
|
+
barIncompleteChar: '░',
|
|
260
|
+
fps: 10,
|
|
261
|
+
stream: process.stdout,
|
|
262
|
+
position: 'center',
|
|
263
|
+
schedule: function(schedule) {
|
|
264
|
+
return schedule;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
progressBar.start(100, 0);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const response = await axios({
|
|
272
|
+
method: 'get',
|
|
273
|
+
url: song.url,
|
|
274
|
+
responseType: 'stream',
|
|
275
|
+
timeout: 60000,
|
|
276
|
+
onDownloadProgress: (progressEvent) => {
|
|
277
|
+
const percent = Math.round((progressEvent.loaded * 100) / (progressEvent.total || progressEvent.loaded));
|
|
278
|
+
progressBar.update(percent);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const totalLength = parseInt(response.headers['content-length'] || '0', 10);
|
|
283
|
+
|
|
284
|
+
const writer = fs.createWriteStream(filePath);
|
|
285
|
+
|
|
286
|
+
response.data.on('data', (chunk) => {
|
|
287
|
+
progressBar.update(Math.min(99, Math.round((writer.bytesWritten * 100) / (totalLength || writer.bytesWritten))));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
response.data.pipe(writer);
|
|
291
|
+
|
|
292
|
+
await new Promise((resolve, reject) => {
|
|
293
|
+
writer.on('finish', resolve);
|
|
294
|
+
writer.on('error', reject);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
progressBar.stop();
|
|
298
|
+
|
|
299
|
+
this.history.push({
|
|
300
|
+
title: song.title,
|
|
301
|
+
artist: song.artist,
|
|
302
|
+
album: song.album,
|
|
303
|
+
platform: song.platform,
|
|
304
|
+
filePath: filePath,
|
|
305
|
+
downloadDate: new Date().toISOString()
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
console.log(chalk.green(`\n✅ 下载成功: ${song.artist} - ${song.title}`));
|
|
309
|
+
console.log(chalk.gray(` 📁 保存至: ${filePath}`));
|
|
310
|
+
|
|
311
|
+
return filePath;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
progressBar.stop();
|
|
314
|
+
console.error(chalk.red(`\n❌ 下载失败: ${song.title}`));
|
|
315
|
+
console.error(chalk.gray(` 错误信息: ${error.message}`));
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
sanitizeFileName(fileName) {
|
|
321
|
+
return fileName
|
|
322
|
+
.replace(/[<>:"/\\|?*]/g, '-')
|
|
323
|
+
.replace(/\s+/g, ' ')
|
|
324
|
+
.trim()
|
|
325
|
+
.slice(0, 200);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getHistory() {
|
|
329
|
+
return this.history;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
clearHistory() {
|
|
333
|
+
this.history = [];
|
|
334
|
+
console.log(chalk.cyan('\n🗑️ 下载历史已清空'));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { MusicDownloader };
|