Files
163-music-dl/163_music_downloader.js
T
2026-07-22 17:52:55 +08:00

2074 lines
86 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { execSync } = require('child_process');
const {
API,
DOWNLOAD_DIR,
CONFIG_FILE,
TRASH_DIR,
TEMP_ZIP_DIR,
QUALITY_LABELS,
DEFAULT_LEVEL,
DEFAULT_RETRIES,
getConfig,
setConfig,
} = require('./lib/config');
const { fetchJSON, downloadFile } = require('./lib/network');
const { sanitize, sleep, getDirSize } = require('./lib/utils');
const { togglePause, waitIfPaused } = require('./lib/pause');
const { loadHistory, saveHistory, addHistory, formatTime } = require('./lib/history');
const { hasFFmpeg, hasNodeID3, embedCover } = require('./lib/tagging');
const { fetchLyric, mergeLrc } = require('./lib/lyrics');
const {
fetchNeteaseSongDetail,
fetchNeteaseAlbumDetail,
getAlbumDownloadTarget,
saveAlbumCover,
fetchFullMetadata,
} = require('./lib/metadata');
const { parseLevel, printQualityMenu, askLevel } = require('./lib/quality');
const { searchSongs, interactiveSearch, fetchPlaylist } = require('./lib/catalog');
const { downloadSong } = require('./lib/downloader');
function createRL() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise(r => rl.question(q, r));
return { rl, ask };
}
function printBanner() {
console.log('');
console.log('╔══════════════════════════════════════════╗');
console.log('║ 🎵 网易云音乐下载器 v2.0 ║');
console.log('║ 基于 ChKSz API · 免费 · 高音质 ║');
console.log('╚══════════════════════════════════════════╝');
console.log(` 封面嵌入: node-id3(${hasNodeID3() ? '✓' : '✗'}) ffmpeg(${hasFFmpeg() ? '✓' : '✗'})`);
console.log(` 下载目录: ${DOWNLOAD_DIR}`);
console.log('');
}
function printMenu() {
console.log('┌────────────────────────────┐');
console.log('│ 1. 搜索歌曲并下载 │');
console.log('│ 2. 输入歌曲 ID 下载 │');
console.log('│ 3. 输入歌单 ID 下载全部 │');
console.log('│ 4. 查看歌单歌曲列表 │');
console.log('│ 5. 查看歌曲歌词 │');
console.log('│ 6. 按歌手批量下载 │');
console.log('│ 7. 试听歌曲 │');
console.log('│ 8. 查看歌曲详情 │');
console.log('│ 9. 下载历史 │');
console.log('│ 0. 设置音质等级 │');
console.log('│ s. 按歌名搜索 │');
console.log('│ a. 下载完整专辑 │');
console.log('│ b. 批量ID下载 │');
console.log('│ f. 从文件批量下载 │');
console.log('│ n. 从文件名识别并补全标签 │');
console.log('│ e. 导出下载历史 │');
console.log('│ m. 保存歌单为 M3U │');
console.log('│ g. 自动识别并补全标签 │');
console.log('│ z. 歌单下载并打包 ZIP │');
console.log('│ c. 清理缓存和临时文件 │');
console.log('│ t. 下载统计 │');
console.log('│ l. 已下载歌曲列表 │');
console.log('│ p. 暂停/恢复下载 │');
console.log('│ r. 重置所有设置 │');
console.log('│ h. 帮助 │');
console.log('│ q. 退出 │');
console.log('└────────────────────────────┘');
}
async function main() {
const args = process.argv.slice(2);
// 解析命令行参数
let level = getConfig('level', DEFAULT_LEVEL);
const ids = [];
let playlistId = null;
let albumId = null;
let batchLyric = true;
let batchCover = true;
let maxRetries = DEFAULT_RETRIES;
for (const arg of args) {
if (arg.startsWith('--level=')) level = parseLevel(arg.split('=')[1]) || level;
else if (arg.startsWith('--playlist=')) playlistId = arg.split('=')[1];
else if (arg.startsWith('--album=')) albumId = arg.split('=')[1];
else if (arg.startsWith('--retries=')) maxRetries = parseInt(arg.split('=')[1], 10) || DEFAULT_RETRIES;
else if (arg === '--no-lyric') batchLyric = false;
else if (arg === '--no-cover') batchCover = false;
else if (/^\d+$/.test(arg)) ids.push(arg);
}
// 命令行批量模式
if (ids.length || playlistId || albumId) {
printBanner();
console.log('📋 命令行模式\n');
if (albumId) {
const album = await fetchNeteaseAlbumDetail(albumId);
if (!album?.tracks?.length) {
console.log('❌ 专辑获取失败或没有歌曲');
} else {
console.log(`💿 专辑: ${album.name} - ${album.artist?.name || '未知歌手'} (${album.tracks.length} 首)\n`);
if (batchCover) {
const albumTarget = getAlbumDownloadTarget(album, album.tracks[0], 0);
await saveAlbumCover(album.picUrl, albumTarget.outputDir);
}
for (let i = 0; i < album.tracks.length; i++) {
const track = album.tracks[i];
const target = getAlbumDownloadTarget(album, track, i);
await waitIfPaused();
const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知';
console.log(`[${i + 1}/${album.tracks.length}] ${track.name} - ${artists}`);
try {
await downloadSong(track.id, level, {
downloadLyric: batchLyric,
embedCoverArt: batchCover,
maxRetries,
...target,
});
}
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < album.tracks.length - 1) await sleep(500);
}
}
}
if (playlistId) {
const pl = await fetchPlaylist(playlistId);
if (pl) {
console.log(`📜 歌单: ${pl.name} (${pl.trackCount} 首)\n`);
for (let i = 0; i < pl.tracks.length; i++) {
const t = pl.tracks[i];
await waitIfPaused();
console.log(`[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`);
try { await downloadSong(t.id, level, { downloadLyric: batchLyric, embedCoverArt: batchCover, maxRetries }); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < pl.tracks.length - 1) await sleep(500);
}
}
}
for (const id of ids) {
await waitIfPaused();
try { await downloadSong(id, level, { downloadLyric: batchLyric, embedCoverArt: batchCover, maxRetries }); }
catch (e) { console.error(`❌ ID ${id} 出错: ${e.message}`); }
}
return;
}
// 交互模式
printBanner();
const { rl, ask } = createRL();
// 设置快捷键监听
if (process.stdin.isTTY) {
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key && key.name === 'p' && !key.ctrl && !key.meta) {
togglePause();
}
if (key && key.ctrl && key.name === 'c') {
process.exit(0);
}
});
}
// 启用 keypress 事件用于暂停快捷键
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key && key.name === 'p' && !isPaused) {
// 只在非输入状态下触发暂停
}
if (key && key.ctrl && key.name === 'c') {
process.exit(0);
}
});
while (true) {
printMenu();
const choice = await ask('\n请选择功能: ');
switch (choice.trim()) {
case '1': {
// 搜索并下载
const songs = await interactiveSearch(rl, ask);
if (songs?.length) {
level = await askLevel(ask, level);
for (const s of songs) {
await waitIfPaused();
try { await downloadSong(s.id, level); }
catch (e) { console.error(`${s.name} 出错: ${e.message}`); }
await sleep(500);
}
}
break;
}
case '2': {
// 按 ID 下载
const input = await ask('请输入歌曲ID (多个用逗号分隔): ');
const songIds = input.split(/[,\s]+/).filter(s => /^\d+$/.test(s.trim()));
if (songIds.length) {
level = await askLevel(ask, level);
for (const id of songIds) {
await waitIfPaused();
try { await downloadSong(id, level); }
catch (e) { console.error(`❌ ID ${id} 出错: ${e.message}`); }
await sleep(500);
}
}
break;
}
case '3': {
// 歌单下载
const plInput = await ask('请输入歌单ID: ');
const plId = plInput.trim();
if (!/^\d+$/.test(plId)) { console.log('⚠️ 无效歌单ID'); break; }
const pl = await fetchPlaylist(plId);
if (!pl) break;
console.log(`\n📜 歌单: ${pl.name} (${pl.trackCount} 首)`);
level = await askLevel(ask, level);
const confirm = await ask(`确认下载全部 ${pl.trackCount} 首? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
for (let i = 0; i < pl.tracks.length; i++) {
const t = pl.tracks[i];
await waitIfPaused();
console.log(`\n[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`);
try { await downloadSong(t.id, level); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < pl.tracks.length - 1) await sleep(500);
}
break;
}
case '4': {
// 查看歌单歌曲列表
const plInput = await ask('请输入歌单ID: ');
const plId = plInput.trim();
if (!/^\d+$/.test(plId)) { console.log('⚠️ 无效歌单ID'); break; }
const pl = await fetchPlaylist(plId);
if (!pl) break;
console.log(`\n📜 ${pl.name} (${pl.trackCount} 首)`);
console.log(` 创建者: ${pl.creator?.nickname || '未知'}`);
if (pl.description) console.log(` 描述: ${pl.description.slice(0, 100)}`);
console.log(' ' + '─'.repeat(60));
console.log(' # 歌曲名 歌手 专辑');
console.log(' ' + '─'.repeat(60));
pl.tracks.forEach((t, i) => {
const num = String(i + 1).padStart(3);
const name = (t.name || '').slice(0, 22).padEnd(24);
const artist = (t.ar?.map(a => a.name).join('/') || '').slice(0, 18).padEnd(20);
const album = (t.al?.name || '').slice(0, 18);
console.log(` ${num} ${name} ${artist} ${album}`);
});
console.log(' ' + '─'.repeat(60));
console.log(`${pl.trackCount}`);
console.log('\n 下载选项:');
console.log(' a = 下载全部');
console.log(' 输入序号 = 下载指定歌曲 (如: 1,3,5 或 5-10)');
console.log(' 回车 = 跳过');
const doDownload = await ask(' 请选择: ');
const dlInput = doDownload.trim().toLowerCase();
let tracksToDownload = [];
if (dlInput === 'a') {
tracksToDownload = pl.tracks;
} else if (dlInput) {
// 解析序号: 支持 1,3,5 和 5-10 混合
const indices = new Set();
for (const part of dlInput.split(/[,\s]+/)) {
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1], 10);
const end = parseInt(rangeMatch[2], 10);
for (let n = start; n <= end; n++) indices.add(n);
} else if (/^\d+$/.test(part)) {
indices.add(parseInt(part, 10));
}
}
tracksToDownload = [...indices]
.filter(n => n >= 1 && n <= pl.tracks.length)
.sort((a, b) => a - b)
.map(n => pl.tracks[n - 1]);
}
if (tracksToDownload.length) {
level = await askLevel(ask, level);
console.log(`\n📥 开始下载 ${tracksToDownload.length} 首歌曲...`);
for (let i = 0; i < tracksToDownload.length; i++) {
const t = tracksToDownload[i];
console.log(`\n[${i + 1}/${tracksToDownload.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`);
try { await downloadSong(t.id, level); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < tracksToDownload.length - 1) await sleep(500);
}
}
break;
}
case '5': {
// 查看歌曲歌词
const idInput = await ask('请输入歌曲ID: ');
const songId = idInput.trim();
if (!/^\d+$/.test(songId)) { console.log('⚠️ 无效歌曲ID'); break; }
console.log(`\n🔍 获取歌词 (ID: ${songId}) ...`);
const data = await fetchLyric(songId);
if (!data?.lrc) {
console.log('❌ 未获取到歌词');
break;
}
// 解析歌词用于显示
const parseLrc = (lrcStr) => {
return lrcStr.split('\n')
.map(line => {
const m = line.match(/^\[(\d+):(\d+)[.:](\d+)\](.*)/);
if (!m) return null;
const time = `${m[1]}:${m[2]}`;
const text = m[4].trim();
if (!text) return null;
return { time, text };
})
.filter(Boolean);
};
const lrcLines = parseLrc(data.lrc);
const transLines = data.tlyric ? parseLrc(data.tlyric) : [];
const transMap = new Map(transLines.map(l => [l.time, l.text]));
console.log('\n 📜 歌词:\n');
for (const line of lrcLines) {
console.log(` [${line.time}] ${line.text}`);
if (transMap.has(line.time)) {
console.log(`${transMap.get(line.time)}`);
}
}
const saveOpt = await ask('\n是否保存为 .lrc 文件? (y/n): ');
if (saveOpt.trim().toLowerCase() === 'y') {
const defaultName = `song_${songId}`;
const nameInput = await ask(`文件名 (默认 ${defaultName}): `);
const baseName = sanitize(nameInput.trim() || defaultName);
const lrcPath = path.join(DOWNLOAD_DIR, baseName + '.lrc');
fs.writeFileSync(lrcPath, data.lrc, 'utf-8');
console.log(` ✅ 歌词已保存: ${lrcPath}`);
if (data.tlyric && data.tlyric.trim()) {
const merged = mergeLrc(data.lrc, data.tlyric);
const mergedPath = path.join(DOWNLOAD_DIR, baseName + '.合并翻译.lrc');
fs.writeFileSync(mergedPath, merged, 'utf-8');
console.log(` ✅ 翻译歌词已保存: ${mergedPath}`);
}
}
break;
}
case '6': {
// 按歌手批量下载
const artistInput = await ask('请输入歌手名: ');
const artistName = artistInput.trim();
if (!artistName) { console.log('⚠️ 请输入歌手名'); break; }
const limitInput = await ask('搜索数量 (默认 50): ');
const searchLimit = parseInt(limitInput, 10) || 50;
console.log(`\n🔍 搜索 "${artistName}" 的歌曲 (最多 ${searchLimit} 首) ...`);
const results = await searchSongs(artistName, searchLimit);
if (!results.length) break;
// 过滤出歌手名匹配的歌曲
const artistLower = artistName.toLowerCase();
const matched = results.filter(s => s.artists.toLowerCase().includes(artistLower));
const pool = matched.length >= 3 ? matched : results; // 匹配太少就用全部
if (matched.length >= 3 && matched.length < results.length) {
console.log(` 找到 ${results.length} 首结果,其中 ${matched.length} 首匹配歌手 "${artistName}"`);
}
console.log(`\n # 歌曲名 歌手 专辑`);
console.log(' ' + '─'.repeat(70));
pool.slice(0, 30).forEach((s, i) => {
const num = String(i + 1).padStart(2);
const name = s.name.slice(0, 20).padEnd(22);
const artist = s.artists.slice(0, 16).padEnd(18);
console.log(` ${num} ${name} ${artist} ${s.album}`);
});
if (pool.length > 30) console.log(` ... 共 ${pool.length} 首,仅显示前 30 首`);
console.log('\n 下载选项:');
console.log(' a = 下载全部');
console.log(' 输入序号 = 下载指定歌曲 (如: 1,3,5 或 1-10)');
console.log(' 回车 = 跳过');
const pick = await ask(' 请选择: ');
const pickInput = pick.trim().toLowerCase();
let toDownload = [];
if (pickInput === 'a') {
toDownload = pool;
} else if (pickInput) {
const indices = new Set();
for (const part of pickInput.split(/[,\s]+/)) {
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1], 10);
const end = parseInt(rangeMatch[2], 10);
for (let n = start; n <= end; n++) indices.add(n);
} else if (/^\d+$/.test(part)) {
indices.add(parseInt(part, 10));
}
}
toDownload = [...indices]
.filter(n => n >= 1 && n <= pool.length)
.sort((a, b) => a - b)
.map(n => pool[n - 1]);
}
if (toDownload.length) {
level = await askLevel(ask, level);
console.log(`\n📥 开始下载 ${toDownload.length} 首歌曲...`);
for (let i = 0; i < toDownload.length; i++) {
const s = toDownload[i];
console.log(`\n[${i + 1}/${toDownload.length}] ${s.name} - ${s.artists}`);
try { await downloadSong(s.id, level); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < toDownload.length - 1) await sleep(500);
}
}
break;
}
case '7': {
// 试听歌曲
const idInput = await ask('请输入歌曲ID: ');
const songId = idInput.trim();
if (!/^\d+$/.test(songId)) { console.log('⚠️ 无效歌曲ID'); break; }
console.log(`\n🔍 获取试听链接 (ID: ${songId}) ...`);
try {
const result = await fetchJSON(`${API.music}?id=${songId}&level=standard`);
if (result.code !== 200 || !result.data?.url) {
console.log(`❌ 获取失败: ${result.msg || '未知错误'}`);
break;
}
const { name, artist, album, url: audioUrl } = result.data;
console.log(` 🎶 ${name} - ${artist} (${album})`);
// 检测可用播放器
const players = [
{ cmd: 'mpv', args: (u) => ['--no-video', u] },
{ cmd: 'ffplay', args: (u) => ['-nodisp', '-autoexit', u] },
{ cmd: 'play', args: (u) => [u] }, // sox
{ cmd: 'cvlc', args: (u) => ['--play-and-exit', u] },
{ cmd: 'aplay', args: (u) => [u] },
];
let player = null;
for (const p of players) {
try {
execSync(`which ${p.cmd}`, { stdio: 'ignore' });
player = p;
break;
} catch {}
}
if (!player) {
console.log(' ⚠️ 未检测到音频播放器 (mpv/ffplay/play/cvlc/aplay)');
console.log(` 📋 试听链接: ${audioUrl}`);
console.log(' 请手动在浏览器或播放器中打开');
break;
}
console.log(` ▶️ 使用 ${player.cmd} 播放中... (Ctrl+C 停止)`);
try {
execSync(`${player.cmd} ${player.args(audioUrl).map(a => `"${a}"`).join(' ')}`, {
stdio: 'inherit',
timeout: 300000, // 5分钟超时
});
} catch (e) {
if (e.status !== null) {
// 用户中断或正常结束
} else {
console.error(` ❌ 播放出错: ${e.message}`);
}
}
console.log(' ⏹ 播放结束');
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
}
break;
}
case '8': {
// 查看歌曲详情(完整元数据)
const idInput = await ask('请输入歌曲ID: ');
const songId = idInput.trim();
if (!/^\d+$/.test(songId)) { console.log('⚠️ 无效歌曲ID'); break; }
console.log(`\n🔍 获取歌曲详情 (ID: ${songId}) ...`);
try {
const meta = await fetchFullMetadata(songId);
if (!meta) {
console.log('❌ 获取失败');
break;
}
const pad = 42;
console.log('');
console.log(' ╔══════════════════════════════════════════════════╗');
console.log(` ║ 🎵 ${meta.name}`);
if (meta.aliases.length) {
console.log(` ║ 别名: ${meta.aliases.join(', ')}`);
}
if (meta.transNames.length) {
console.log(` ║ 译名: ${meta.transNames.join(', ')}`);
}
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(` ║ 歌手: ${meta.artistStr}`);
console.log(` ║ 专辑: ${meta.albumStr}`);
console.log(` ║ ID: ${meta.id}`);
if (meta.trackNo) console.log(` ║ 曲目号: ${meta.trackNo}${meta.disc ? ` (碟片 ${meta.disc})` : ''}`);
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(` ║ 时长: ${meta.durationStr}`);
if (meta.publishDate) {
console.log(` ║ 发行日期: ${meta.publishDate}`);
} else if (meta.publishYear) {
console.log(` ║ 发行年份: ${meta.publishYear}`);
}
if (meta.company) console.log(` ║ 唱片公司: ${meta.company}`);
if (meta.albumType) console.log(` ║ 专辑类型: ${meta.albumType}`);
if (meta.albumSize) console.log(` ║ 专辑曲目: ${meta.albumSize}`);
if (meta.tags.length) console.log(` ║ 标签: ${meta.tags.join(', ')}`);
console.log(` ║ 热度: ${'★'.repeat(Math.round(meta.popularity / 20))}${'☆'.repeat(5 - Math.round(meta.popularity / 20))} (${meta.popularity}/100)`);
console.log(' ╠══════════════════════════════════════════════════╣');
// 音质信息
if (meta.quality) {
console.log(' ║ 音质信息:');
const q = meta.quality;
if (q.l) console.log(` ║ 标准 ${q.l.br}kbps ${(q.l.size / 1048576).toFixed(1)} MB ${q.l.sr}Hz`);
if (q.m) console.log(` ║ 较高 ${q.m.br}kbps ${(q.m.size / 1048576).toFixed(1)} MB ${q.m.sr}Hz`);
if (q.h) console.log(` ║ 极高 ${q.h.br}kbps ${(q.h.size / 1048576).toFixed(1)} MB ${q.h.sr}Hz`);
}
// ChKSz 最高音质
if (meta.chksz) {
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(` ║ ChKSz 最高可用: ${meta.chksz.level} (${meta.chksz.br}kbps) ${(meta.chksz.size / 1048576).toFixed(1)} MB`);
}
// 专辑描述
if (meta.description) {
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(' ║ 专辑简介:');
const descLines = meta.description.split(/\n/).slice(0, 3);
for (const line of descLines) {
const trimmed = line.trim();
if (trimmed) {
// 自动换行
for (let i = 0; i < trimmed.length; i += 44) {
console.log(`${trimmed.slice(i, i + 44)}`);
}
}
}
if (meta.description.split(/\n/).length > 3) {
console.log(' ║ ...');
}
}
console.log(' ╚══════════════════════════════════════════════════╝');
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
}
break;
}
case '9': {
// 下载历史
const history = loadHistory();
if (!history.length) {
console.log('\n 📭 暂无下载历史');
break;
}
const showCount = Math.min(history.length, 30);
console.log(`\n 📜 下载历史 (最近 ${showCount} 条,共 ${history.length} 条)`);
console.log(' ' + '─'.repeat(70));
console.log(' # 时间 歌曲名 歌手 音质');
console.log(' ' + '─'.repeat(70));
history.slice(0, showCount).forEach((h, i) => {
const num = String(i + 1).padStart(3);
const time = formatTime(h.timestamp);
const name = (h.name || '').slice(0, 20).padEnd(22);
const artist = (h.artist || '').slice(0, 14).padEnd(16);
const lv = h.level || '?';
console.log(` ${num} ${time} ${name} ${artist} ${lv}`);
});
console.log(' ' + '─'.repeat(70));
if (history.length > showCount) {
console.log(` ... 还有 ${history.length - showCount} 条更早的记录`);
}
const action = await ask('\n 操作: [d=删除指定] [c=清空全部] [回车=返回]: ');
if (action.trim().toLowerCase() === 'c') {
const confirm = await ask(' ⚠️ 确认清空全部下载历史? (输入 yes 确认): ');
if (confirm.trim() === 'yes') {
saveHistory([]);
console.log(' ✅ 下载历史已全部清空');
} else {
console.log(' 取消操作');
}
} else if (action.trim().toLowerCase() === 'd') {
const delInput = await ask(' 输入要删除的序号 (如: 1,3,5): ');
const delIndices = delInput.split(/[,\s]+/)
.map(n => parseInt(n, 10))
.filter(n => n >= 1 && n <= showCount)
.sort((a, b) => b - a); // 从后往前删
for (const idx of delIndices) {
history.splice(idx - 1, 1);
}
saveHistory(history);
console.log(` ✅ 已删除 ${delIndices.length} 条记录`);
}
break;
}
case '0': {
// 设置音质
const label = QUALITY_LABELS[level] ? ` (${QUALITY_LABELS[level]})` : '';
console.log(`\n当前音质: ${level}${label}`);
console.log('可选音质:');
printQualityMenu(level);
const newLevel = await ask('选择音质等级 (输入序号或名称): ');
const parsed = parseLevel(newLevel);
if (parsed) {
level = parsed;
setConfig('level', level);
console.log(`✅ 音质已设为: ${level}${QUALITY_LABELS[level] ? ` (${QUALITY_LABELS[level]})` : ''} (已保存)`);
} else {
console.log('⚠️ 无效等级');
}
break;
}
case 's':
case 'S': {
// 按歌名搜索
const songName = await ask('请输入歌名: ');
if (!songName.trim()) { console.log('⚠️ 请输入歌名'); break; }
const limitInput = await ask('搜索数量 (默认 20): ');
const searchLimit = parseInt(limitInput, 10) || 20;
console.log(`\n🔍 搜索 "${songName.trim()}" ...`);
const results = await searchSongs(songName.trim(), searchLimit);
if (!results.length) break;
console.log(`\n # 歌曲名 歌手 专辑`);
console.log(' ' + '─'.repeat(70));
results.forEach((s, i) => {
const num = String(i + 1).padStart(2);
const name = s.name.slice(0, 20).padEnd(22);
const artist = s.artists.slice(0, 16).padEnd(18);
const album = s.album.slice(0, 20);
console.log(` ${num} ${name} ${artist} ${album}`);
});
console.log(' ' + '─'.repeat(70));
console.log(`${results.length} 首结果`);
console.log('\n 操作选项:');
console.log(' 输入序号 = 下载指定歌曲 (如: 1,3,5 或 1-5)');
console.log(' a = 下载全部');
console.log(' v+序号 = 试听 (如: v3)');
console.log(' d+序号 = 查看详情 (如: d3)');
console.log(' 回车 = 返回');
const action = await ask(' 请选择: ');
const actionInput = action.trim().toLowerCase();
if (!actionInput) break;
// 试听
const listenMatch = actionInput.match(/^v(\d+)$/);
if (listenMatch) {
const idx = parseInt(listenMatch[1], 10);
if (idx >= 1 && idx <= results.length) {
const s = results[idx - 1];
console.log(`\n ▶️ 试听: ${s.name} - ${s.artists}`);
try {
const r = await fetchJSON(`${API.music}?id=${s.id}&level=standard`);
if (r.code === 200 && r.data?.url) {
const players = [
{ cmd: 'mpv', args: (u) => ['--no-video', u] },
{ cmd: 'ffplay', args: (u) => ['-nodisp', '-autoexit', u] },
{ cmd: 'play', args: (u) => [u] },
{ cmd: 'cvlc', args: (u) => ['--play-and-exit', u] },
];
let player = null;
for (const p of players) {
try { execSync(`which ${p.cmd}`, { stdio: 'ignore' }); player = p; break; } catch {}
}
if (player) {
console.log(` ▶️ 使用 ${player.cmd} 播放中... (Ctrl+C 停止)`);
try { execSync(`${player.cmd} ${player.args(r.data.url).map(a => `"${a}"`).join(' ')}`, { stdio: 'inherit', timeout: 300000 }); } catch {}
console.log(' ⏹ 播放结束');
} else {
console.log(` 📋 链接: ${r.data.url}`);
}
}
} catch (e) { console.error(`${e.message}`); }
} else {
console.log(' ⚠️ 无效序号');
}
break;
}
// 详情
const detailMatch = actionInput.match(/^d(\d+)$/);
if (detailMatch) {
const idx = parseInt(detailMatch[1], 10);
if (idx >= 1 && idx <= results.length) {
const s = results[idx - 1];
try {
const r = await fetchJSON(`${API.music}?id=${s.id}&level=jymaster`);
if (r.code === 200 && r.data) {
const d = r.data;
console.log('\n ┌─────────────────────────────────────────┐');
console.log(` │ 🎵 ${d.name}`);
console.log(' ├─────────────────────────────────────────┤');
console.log(` │ 歌手: ${d.artist}`);
console.log(` │ 专辑: ${d.album}`);
console.log(` │ ID: ${d.id}`);
console.log(` │ 音质: ${d.level} (${d.br}kbps) 大小: ${(d.size / 1048576).toFixed(2)} MB`);
console.log(' └─────────────────────────────────────────┘');
}
} catch (e) { console.error(`${e.message}`); }
} else {
console.log(' ⚠️ 无效序号');
}
break;
}
// 下载
let toDownload = [];
if (actionInput === 'a') {
toDownload = results;
} else {
const indices = new Set();
for (const part of actionInput.split(/[,\s]+/)) {
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1], 10);
const end = parseInt(rangeMatch[2], 10);
for (let n = start; n <= end; n++) indices.add(n);
} else if (/^\d+$/.test(part)) {
indices.add(parseInt(part, 10));
}
}
toDownload = [...indices]
.filter(n => n >= 1 && n <= results.length)
.sort((a, b) => a - b)
.map(n => results[n - 1]);
}
if (toDownload.length) {
level = await askLevel(ask, level);
console.log(`\n📥 开始下载 ${toDownload.length} 首歌曲...`);
for (let i = 0; i < toDownload.length; i++) {
const s = toDownload[i];
console.log(`\n[${i + 1}/${toDownload.length}] ${s.name} - ${s.artists}`);
try { await downloadSong(s.id, level); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < toDownload.length - 1) await sleep(500);
}
}
break;
}
case 'l':
case 'L': {
// 已下载歌曲列表
if (!fs.existsSync(DOWNLOAD_DIR)) {
console.log('\n 📭 下载目录不存在');
break;
}
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const audioExts = ['.mp3', '.flac', '.m4a', '.wav', '.aac', '.ogg', '.wma'];
const audioFiles = allFiles.filter(f => audioExts.includes(path.extname(f).toLowerCase()));
if (!audioFiles.length) {
console.log('\n 📭 暂无已下载的歌曲');
break;
}
// 按修改时间排序(最新在前)
const filesWithTime = audioFiles.map(f => {
const fp = path.join(DOWNLOAD_DIR, f);
const stat = fs.statSync(fp);
return { name: f, size: stat.size, mtime: stat.mtime };
}).sort((a, b) => b.mtime - a.mtime);
console.log(`\n 📁 已下载歌曲 (${filesWithTime.length} 首)`);
console.log(` 目录: ${DOWNLOAD_DIR}`);
console.log(' ' + '─'.repeat(75));
console.log(' # 文件名 大小 修改时间');
console.log(' ' + '─'.repeat(75));
const showCount = Math.min(filesWithTime.length, 50);
filesWithTime.slice(0, showCount).forEach((f, i) => {
const num = String(i + 1).padStart(3);
const name = f.name.length > 38 ? f.name.slice(0, 35) + '...' : f.name;
const sizeMB = (f.size / 1048576).toFixed(1);
const time = `${f.mtime.getMonth() + 1}/${f.mtime.getDate()} ${String(f.mtime.getHours()).padStart(2, '0')}:${String(f.mtime.getMinutes()).padStart(2, '0')}`;
console.log(` ${num} ${name.padEnd(40)} ${(sizeMB + ' MB').padStart(10)} ${time}`);
});
if (filesWithTime.length > showCount) {
console.log(` ... 还有 ${filesWithTime.length - showCount} 首未显示`);
}
const totalSize = filesWithTime.reduce((s, f) => s + f.size, 0);
console.log(' ' + '─'.repeat(75));
console.log(`${filesWithTime.length} 首,总大小 ${(totalSize / 1073741824).toFixed(2)} GB`);
const action = await ask('\n 操作: [p+序号=播放] [d+序号=删除] [da=删除全部] [回车=返回]: ');
const actLower = action.trim().toLowerCase();
// 播放
const playMatch = actLower.match(/^p(\d+)$/);
if (playMatch) {
const idx = parseInt(playMatch[1], 10);
if (idx >= 1 && idx <= filesWithTime.length) {
const filePath = path.join(DOWNLOAD_DIR, filesWithTime[idx - 1].name);
const players = [
{ cmd: 'mpv', args: (u) => ['--no-video', u] },
{ cmd: 'ffplay', args: (u) => ['-nodisp', '-autoexit', u] },
{ cmd: 'play', args: (u) => [u] },
{ cmd: 'cvlc', args: (u) => ['--play-and-exit', u] },
];
let player = null;
for (const p of players) {
try { execSync(`which ${p.cmd}`, { stdio: 'ignore' }); player = p; break; } catch {}
}
if (player) {
console.log(` ▶️ 播放: ${filesWithTime[idx - 1].name}`);
try { execSync(`${player.cmd} ${player.args(filePath).map(a => `"${a}"`).join(' ')}`, { stdio: 'inherit', timeout: 300000 }); } catch {}
console.log(' ⏹ 播放结束');
} else {
console.log(' ⚠️ 未检测到音频播放器');
}
} else {
console.log(' ⚠️ 无效序号');
}
}
// 删除指定
const delMatch = actLower.match(/^d(\d+)$/);
if (delMatch) {
const idx = parseInt(delMatch[1], 10);
if (idx >= 1 && idx <= filesWithTime.length) {
const target = filesWithTime[idx - 1];
const confirm = await ask(` 确认删除 "${target.name}"? (y/n): `);
if (confirm.trim().toLowerCase() === 'y') {
const filePath = path.join(DOWNLOAD_DIR, target.name);
// 移到回收站目录
if (!fs.existsSync(TRASH_DIR)) fs.mkdirSync(TRASH_DIR, { recursive: true });
const trashPath = path.join(TRASH_DIR, `${Date.now()}_${target.name}`);
try {
fs.renameSync(filePath, trashPath);
console.log(` ✅ 已移到回收站: ${target.name}`);
// 同时移动关联的 .lrc 文件
const baseName = path.parse(target.name).name;
for (const f of allFiles) {
if (f.startsWith(baseName) && f.endsWith('.lrc')) {
const lrcSrc = path.join(DOWNLOAD_DIR, f);
const lrcDst = path.join(TRASH_DIR, `${Date.now()}_${f}`);
try { fs.renameSync(lrcSrc, lrcDst); } catch {}
}
}
} catch (e) {
console.error(` ❌ 删除失败: ${e.message}`);
}
}
} else {
console.log(' ⚠️ 无效序号');
}
}
// 删除全部
if (actLower === 'da') {
const confirm = await ask(` ⚠️ 确认删除全部 ${filesWithTime.length} 首歌曲? 此操作不可恢复! (输入 yes 确认): `);
if (confirm.trim() === 'yes') {
if (!fs.existsSync(TRASH_DIR)) fs.mkdirSync(TRASH_DIR, { recursive: true });
let deleted = 0;
for (const f of filesWithTime) {
try {
const src = path.join(DOWNLOAD_DIR, f.name);
const dst = path.join(TRASH_DIR, `${Date.now()}_${f.name}`);
fs.renameSync(src, dst);
deleted++;
} catch {}
}
// 清理关联文件
for (const f of allFiles) {
if (!audioExts.includes(path.extname(f).toLowerCase())) {
try {
const src = path.join(DOWNLOAD_DIR, f);
const dst = path.join(TRASH_DIR, `${Date.now()}_${f}`);
fs.renameSync(src, dst);
} catch {}
}
}
console.log(` ✅ 已删除 ${deleted} 首歌曲 (已移到 .trash 目录)`);
} else {
console.log(' 取消删除');
}
}
break;
}
case 'a':
case 'A': {
// 按专辑下载
const albumInput = await ask('请输入专辑 ID(推荐)或专辑名: ');
const albumQuery = albumInput.trim();
if (!albumQuery) { console.log('⚠️ 请输入专辑 ID 或名称'); break; }
if (/^\d+$/.test(albumQuery)) {
console.log(`\n🔍 获取专辑 ${albumQuery} ...`);
const album = await fetchNeteaseAlbumDetail(albumQuery);
if (!album?.tracks?.length) {
console.log('❌ 专辑获取失败或没有歌曲');
break;
}
console.log(`\n💿 ${album.name} - ${album.artist?.name || '未知歌手'}`);
console.log(`${album.tracks.length} 首歌曲`);
album.tracks.forEach((track, i) => {
const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知';
console.log(` ${String(i + 1).padStart(2)}. ${track.name} - ${artists}`);
});
level = await askLevel(ask, level);
const confirm = await ask(`确认下载整张专辑? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
const albumTarget = getAlbumDownloadTarget(album, album.tracks[0], 0);
await saveAlbumCover(album.picUrl, albumTarget.outputDir);
for (let i = 0; i < album.tracks.length; i++) {
const track = album.tracks[i];
const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知';
const target = getAlbumDownloadTarget(album, track, i);
await waitIfPaused();
console.log(`\n[${i + 1}/${album.tracks.length}] ${track.name} - ${artists}`);
try { await downloadSong(track.id, level, target); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < album.tracks.length - 1) await sleep(500);
}
break;
}
const albumName = albumQuery;
const limitInput = await ask('搜索数量 (默认 50): ');
const searchLimit = parseInt(limitInput, 10) || 50;
console.log(`\n🔍 搜索专辑 "${albumName}" ...`);
const results = await searchSongs(albumName, searchLimit);
if (!results.length) break;
// 按专辑名过滤
const albumLower = albumName.toLowerCase();
const matched = results.filter(s => (s.album || '').toLowerCase().includes(albumLower));
const pool = matched.length >= 2 ? matched : results;
if (matched.length >= 2 && matched.length < results.length) {
console.log(` 找到 ${results.length} 首结果,其中 ${matched.length} 首匹配专辑 "${albumName}"`);
}
// 按专辑分组
const albumGroups = {};
for (const s of pool) {
const key = s.album || '未知专辑';
if (!albumGroups[key]) albumGroups[key] = [];
albumGroups[key].push(s);
}
console.log('\n 匹配的专辑:');
const albumKeys = Object.keys(albumGroups);
albumKeys.forEach((a, i) => {
const num = String(i + 1).padStart(2);
const songs = albumGroups[a];
const artist = songs[0]?.artists || '未知';
console.log(` ${num} ${a.slice(0, 30).padEnd(32)} ${artist} (${songs.length}首)`);
});
const albumPick = await ask('\n选择专辑序号 (多个用逗号分隔, a=全部, 0=取消): ');
const albumPickInput = albumPick.trim().toLowerCase();
if (!albumPickInput || albumPickInput === '0') break;
let albumsToDownload = [];
if (albumPickInput === 'a') {
albumsToDownload = albumKeys;
} else {
const indices = albumPickInput.split(/[,\s]+/)
.map(n => parseInt(n, 10))
.filter(n => n >= 1 && n <= albumKeys.length);
albumsToDownload = [...new Set(indices)].sort((a, b) => a - b).map(n => albumKeys[n - 1]);
}
if (!albumsToDownload.length) break;
const allSongs = [];
for (const aName of albumsToDownload) {
const albumSongs = albumGroups[aName];
for (let trackIndex = 0; trackIndex < albumSongs.length; trackIndex++) {
const song = albumSongs[trackIndex];
if (!allSongs.find(x => x.song.id === song.id)) {
allSongs.push({
song,
albumName: aName,
trackIndex,
trackCount: albumSongs.length,
});
}
}
}
console.log(`\n💿 共 ${albumsToDownload.length} 张专辑,${allSongs.length} 首歌曲`);
level = await askLevel(ask, level);
const confirm = await ask(`确认下载? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
const savedCoverDirs = new Set();
for (let i = 0; i < allSongs.length; i++) {
const item = allSongs[i];
const s = item.song;
const target = getAlbumDownloadTarget({
name: item.albumName,
artist: { name: s.artists || '未知歌手' },
}, s, item.trackIndex, item.trackCount);
if (!savedCoverDirs.has(target.outputDir)) {
const detail = await fetchNeteaseSongDetail(s.id);
await saveAlbumCover(detail?.album?.cover, target.outputDir);
savedCoverDirs.add(target.outputDir);
}
await waitIfPaused();
console.log(`\n[${i + 1}/${allSongs.length}] ${s.name} - ${s.artists}`);
try { await downloadSong(s.id, level, target); }
catch (e) { console.error(` ❌ 出错: ${e.message}`); }
if (i < allSongs.length - 1) await sleep(500);
}
break;
}
case 'b':
case 'B': {
// 批量ID下载
console.log('\n 📋 批量输入歌曲ID');
console.log(' 支持格式:');
console.log(' - 逗号分隔: 123,456,789');
console.log(' - 空格分隔: 123 456 789');
console.log(' - 每行一个 (输入空行结束)');
const idLines = [];
console.log('');
while (true) {
const line = await ask(' 输入ID (空行结束): ');
if (!line.trim()) break;
idLines.push(line.trim());
}
if (!idLines.length) { console.log(' ⚠️ 未输入任何ID'); break; }
// 解析所有ID
const allIds = [];
for (const line of idLines) {
const parts = line.split(/[,\s]+/).filter(s => /^\d+$/.test(s));
allIds.push(...parts);
}
const uniqueIds = [...new Set(allIds)];
if (!uniqueIds.length) { console.log(' ⚠️ 未找到有效ID'); break; }
console.log(`\n 📊 解析结果: ${uniqueIds.length} 个有效ID`);
level = await askLevel(ask, level);
const confirm = await ask(`确认下载 ${uniqueIds.length} 首歌曲? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
let success = 0;
let fail = 0;
const failedIds = [];
for (let i = 0; i < uniqueIds.length; i++) {
const id = uniqueIds[i];
console.log(`\n [${i + 1}/${uniqueIds.length}] ID: ${id}`);
try {
const ok = await downloadSong(id, level);
if (ok) success++;
else { fail++; failedIds.push(id); }
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
fail++;
failedIds.push(id);
}
if (i < uniqueIds.length - 1) await sleep(500);
}
console.log(`\n 📊 下载完成:`);
console.log(` ✅ 成功: ${success}`);
console.log(` ❌ 失败: ${fail}`);
if (failedIds.length) {
console.log(` 失败ID: ${failedIds.join(', ')}`);
const retry = await ask('\n 是否重试失败的歌曲? (y/n): ');
if (retry.trim().toLowerCase() === 'y') {
console.log('\n 🔄 重试中...');
let retrySuccess = 0;
for (let i = 0; i < failedIds.length; i++) {
const id = failedIds[i];
console.log(` [${i + 1}/${failedIds.length}] ID: ${id}`);
try {
const ok = await downloadSong(id, level);
if (ok) retrySuccess++;
} catch {}
if (i < failedIds.length - 1) await sleep(1000);
}
console.log(` ✅ 重试完成: 成功 ${retrySuccess}`);
}
}
break;
}
case 'f':
case 'F': {
// 从文件批量下载
const filePath = await ask('请输入文件路径 (每行一个歌曲ID或"歌手 - 歌名"): ');
const fPath = filePath.trim();
if (!fPath) { console.log('⚠️ 请输入文件路径'); break; }
if (!fs.existsSync(fPath)) { console.log('❌ 文件不存在'); break; }
const lines = fs.readFileSync(fPath, 'utf-8')
.split('\n')
.map(l => l.trim())
.filter(l => l && !l.startsWith('#'));
if (!lines.length) { console.log('⚠️ 文件为空'); break; }
// 区分 ID 和 "歌手 - 歌名"
const idList = [];
const nameList = [];
for (const line of lines) {
if (/^\d+$/.test(line)) {
idList.push(line);
} else if (line.includes('-')) {
nameList.push(line);
}
}
console.log(`\n📋 解析结果: ${idList.length} 个ID, ${nameList.length} 个歌名`);
// 歌名搜索匹配
const resolvedIds = [];
if (nameList.length) {
console.log('\n🔍 搜索歌名匹配...');
for (const nameLine of nameList) {
const parts = nameLine.split(/\s*[-—]\s*/);
const keyword = parts.length >= 2 ? `${parts[0]} ${parts[1]}` : nameLine;
try {
const res = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=3`);
if (res.code === 200 && res.data?.length) {
const best = res.data[0];
resolvedIds.push(best.id);
console.log(` ✅ "${nameLine}" → ${best.name} - ${best.artists} (ID: ${best.id})`);
} else {
console.log(` ⚠️ "${nameLine}" 未找到匹配`);
}
} catch (e) {
console.log(` ❌ "${nameLine}" 搜索失败: ${e.message}`);
}
await sleep(300);
}
}
const allIds = [...idList, ...resolvedIds];
if (!allIds.length) { console.log('❌ 无有效歌曲ID'); break; }
console.log(`\n📥 共 ${allIds.length} 首歌曲待下载`);
level = await askLevel(ask, level);
const confirm = await ask('确认下载? (y/n): ');
if (confirm.trim().toLowerCase() !== 'y') break;
let success = 0;
let fail = 0;
for (let i = 0; i < allIds.length; i++) {
const id = allIds[i];
console.log(`\n[${i + 1}/${allIds.length}] ID: ${id}`);
try {
const ok = await downloadSong(id, level);
if (ok) success++; else fail++;
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
fail++;
}
if (i < allIds.length - 1) await sleep(500);
}
console.log(`\n📊 下载完成: 成功 ${success} 首, 失败 ${fail}`);
break;
}
case 'n':
case 'N': {
// 从文件名识别并补全标签
if (!fs.existsSync(DOWNLOAD_DIR)) {
console.log('\n 📭 下载目录不存在');
break;
}
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const audioExts = ['.mp3', '.flac', '.m4a', '.wav', '.aac'];
const audioFiles = allFiles.filter(f => audioExts.includes(path.extname(f).toLowerCase()));
if (!audioFiles.length) {
console.log('\n 📭 暂无音频文件');
break;
}
console.log(`\n 📁 扫描到 ${audioFiles.length} 个音频文件,分析文件名...\n`);
const candidates = [];
for (const f of audioFiles) {
const baseName = path.parse(f).name;
// 常见格式: "歌手 - 歌名" 或 "歌手-歌名"
const match = baseName.match(/^(.+?)\s*[-—]\s*(.+)$/);
if (match) {
candidates.push({ file: f, artist: match[1].trim(), name: match[2].trim() });
}
}
if (!candidates.length) {
console.log(' ⚠️ 未识别到 "歌手 - 歌名" 格式的文件');
console.log(' 支持的格式: "周杰伦 - 晴天.mp3" 或 "周杰伦-晴天.flac"');
break;
}
console.log(` 识别到 ${candidates.length} 首歌曲:`);
console.log(' ' + '─'.repeat(60));
console.log(' # 歌手 歌曲名 文件');
console.log(' ' + '─'.repeat(60));
candidates.forEach((c, i) => {
const num = String(i + 1).padStart(3);
const artist = c.artist.slice(0, 18).padEnd(20);
const name = c.name.slice(0, 18).padEnd(20);
const file = c.file.length > 30 ? c.file.slice(0, 27) + '...' : c.file;
console.log(` ${num} ${artist} ${name} ${file}`);
});
console.log(' ' + '─'.repeat(60));
console.log('\n 操作选项:');
console.log(' a = 全部自动匹配并补全标签');
console.log(' 输入序号 = 选择指定文件 (如: 1,3,5)');
console.log(' 回车 = 跳过');
const pick = await ask(' 请选择: ');
const pickInput = pick.trim().toLowerCase();
let targets = [];
if (pickInput === 'a') {
targets = candidates;
} else if (pickInput) {
const indices = new Set();
for (const part of pickInput.split(/[,\s]+/)) {
if (/^\d+$/.test(part)) indices.add(parseInt(part, 10));
}
targets = [...indices]
.filter(n => n >= 1 && n <= candidates.length)
.sort((a, b) => a - b)
.map(n => candidates[n - 1]);
}
if (!targets.length) break;
console.log(`\n🏷️ 开始匹配 ${targets.length} 首歌曲...\n`);
let successCount = 0;
for (let i = 0; i < targets.length; i++) {
const c = targets[i];
console.log(`[${i + 1}/${targets.length}] ${c.artist} - ${c.name}`);
try {
// 搜索匹配
const keyword = `${c.artist} ${c.name}`;
const results = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=5`);
if (results.code !== 200 || !results.data?.length) {
console.log(' ⚠️ 未找到匹配');
continue;
}
// 找最佳匹配
const best = results.data.find(s => {
const sName = (s.name || '').toLowerCase();
const sArtist = (s.artists || '').toLowerCase();
return sName.includes(c.name.toLowerCase()) && sArtist.includes(c.artist.toLowerCase());
}) || results.data[0];
console.log(` ✅ 匹配: ${best.name} - ${best.artists} (ID: ${best.id})`);
// 获取完整歌曲信息并嵌入标签
const filePath = path.join(DOWNLOAD_DIR, c.file);
const songInfo = await fetchJSON(`${API.music}?id=${best.id}&level=standard`);
if (songInfo.code === 200 && songInfo.data) {
const d = songInfo.data;
if (d.picUrl) {
await embedCover(filePath, d.picUrl, { name: d.name, artist: d.artist, album: d.album });
}
// 获取并保存歌词
const lyricData = await fetchLyric(best.id);
if (lyricData?.lrc) {
const baseName = path.parse(c.file).name;
const lrcPath = path.join(DOWNLOAD_DIR, baseName + '.lrc');
fs.writeFileSync(lrcPath, lyricData.lrc, 'utf-8');
console.log(` 📄 歌词已保存`);
}
// 重命名文件
const ext = path.extname(c.file);
const newName = sanitize(`${d.artist} - ${d.name}${ext}`);
if (newName !== c.file) {
const newPath = path.join(DOWNLOAD_DIR, newName);
if (!fs.existsSync(newPath)) {
fs.renameSync(filePath, newPath);
console.log(` 📝 重命名: ${newName}`);
}
}
// 记录历史
addHistory({ id: best.id, name: d.name, artist: d.artist, album: d.album, level: 'standard', bitrate: d.br, size: d.size, file: newName });
successCount++;
}
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
}
if (i < targets.length - 1) await sleep(500);
}
console.log(`\n✅ 完成! 成功补全 ${successCount}/${targets.length}`);
break;
}
case 'e':
case 'E': {
// 导出下载历史
const history = loadHistory();
if (!history.length) {
console.log('\n 📭 暂无下载历史可导出');
break;
}
console.log('\n 导出格式:');
console.log(' 1. CSV (Excel 兼容)');
console.log(' 2. JSON');
console.log(' 3. TXT (纯文本列表)');
const fmt = await ask(' 选择格式 (1/2/3): ');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
let exportPath;
switch (fmt.trim()) {
case '1': {
// CSV
exportPath = path.join(__dirname, `download_history_${timestamp}.csv`);
const header = '歌曲ID,歌曲名,歌手,专辑,音质,码率(kbps),文件大小(bytes),文件名,下载时间\n';
const rows = history.map(h => {
return [
h.id,
`"${(h.name || '').replace(/"/g, '""')}"`,
`"${(h.artist || '').replace(/"/g, '""')}"`,
`"${(h.album || '').replace(/"/g, '""')}"`,
h.level || '',
h.bitrate || '',
h.size || '',
`"${(h.file || '').replace(/"/g, '""')}"`,
h.timestamp || '',
].join(',');
}).join('\n');
fs.writeFileSync(exportPath, header + rows + '\n', 'utf-8');
break;
}
case '2': {
// JSON
exportPath = path.join(__dirname, `download_history_${timestamp}.json`);
fs.writeFileSync(exportPath, JSON.stringify(history, null, 2), 'utf-8');
break;
}
case '3': {
// TXT
exportPath = path.join(__dirname, `download_history_${timestamp}.txt`);
const lines = history.map((h, i) => {
const time = h.timestamp ? formatTime(h.timestamp) : '未知';
return `${i + 1}. ${h.artist || '未知'} - ${h.name || '未知'} (${h.album || '未知专辑'}) [${h.level || '?'}] ${time}`;
});
fs.writeFileSync(exportPath, lines.join('\n') + '\n', 'utf-8');
break;
}
default:
console.log(' ⚠️ 无效选项');
break;
}
if (exportPath && fs.existsSync(exportPath)) {
const stat = fs.statSync(exportPath);
console.log(`\n ✅ 导出成功!`);
console.log(` 📄 文件: ${exportPath}`);
console.log(` 📊 共 ${history.length} 条记录, ${(stat.size / 1024).toFixed(1)} KB`);
}
break;
}
case 'm':
case 'M': {
// 保存歌单为 M3U
const plInput = await ask('请输入歌单ID: ');
const plId = plInput.trim();
if (!/^\d+$/.test(plId)) { console.log('⚠️ 无效歌单ID'); break; }
console.log(`\n🔍 获取歌单信息 ...`);
const pl = await fetchPlaylist(plId);
if (!pl) break;
console.log(`📜 歌单: ${pl.name} (${pl.trackCount} 首)`);
// 生成 M3U 内容
const m3uLines = ['#EXTM3U', ''];
for (const t of pl.tracks) {
const artist = t.ar?.map(a => a.name).join('/') || '未知';
const title = t.name || '未知';
const duration = Math.round((t.dt || 0) / 1000) || -1;
m3uLines.push(`#EXTINF:${duration},${artist} - ${title}`);
// 检查本地是否已下载
const localFile = path.join(DOWNLOAD_DIR, sanitize(`${artist} - ${title}`));
const possibleExts = ['.mp3', '.flac', '.m4a', '.wav', '.aac'];
let found = false;
for (const ext of possibleExts) {
const fp = localFile + ext;
if (fs.existsSync(fp)) {
m3uLines.push(fp);
found = true;
break;
}
}
if (!found) {
// 用 API 链接作为备选
m3uLines.push(`#EXTVLCOPT:network-caching=1000`);
m3uLines.push(`${API.music}?id=${t.id}&type=down`);
}
m3uLines.push('');
}
const m3uContent = m3uLines.join('\n');
const defaultName = sanitize(pl.name || `playlist_${plId}`);
const nameInput = await ask(`保存文件名 (默认 ${defaultName}): `);
const fileName = sanitize(nameInput.trim() || defaultName) + '.m3u';
const savePath = path.join(__dirname, fileName);
fs.writeFileSync(savePath, m3uContent, 'utf-8');
console.log(`\n ✅ M3U 播放列表已保存!`);
console.log(` 📄 文件: ${savePath}`);
console.log(` 📊 共 ${pl.trackCount} 首歌曲`);
// 统计本地已有数量
const localCount = m3uLines.filter(l => l.startsWith(DOWNLOAD_DIR)).length;
if (localCount > 0) {
console.log(` 💾 本地已有: ${localCount} 首可直接播放`);
}
break;
}
case 'g':
case 'G': {
// 自动识别并补全标签
if (!fs.existsSync(DOWNLOAD_DIR)) {
console.log('\n 📭 下载目录不存在');
break;
}
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const audioExts = ['.mp3', '.flac', '.m4a', '.wav', '.aac'];
const audioFiles = allFiles.filter(f => audioExts.includes(path.extname(f).toLowerCase()));
if (!audioFiles.length) {
console.log('\n 📭 暂无音频文件');
break;
}
// 筛选需要补全的文件(没有封面或没有歌词)
console.log(`\n 🔍 扫描 ${audioFiles.length} 个音频文件,检查标签完整性...\n`);
const needTag = [];
for (const f of audioFiles) {
const filePath = path.join(DOWNLOAD_DIR, f);
const baseName = path.parse(f).name;
const hasLrc = allFiles.some(lf => lf.startsWith(baseName) && lf.endsWith('.lrc'));
// 简单判断:文件名包含 " - " 且没有对应 .lrc 文件
const isNameFormat = /^.+\s*[-—]\s*.+$/.test(baseName);
if (isNameFormat && !hasLrc) {
needTag.push(f);
}
}
if (!needTag.length) {
console.log(' ✅ 所有文件标签已完整,无需补全');
break;
}
console.log(` 发现 ${needTag.length} 个文件需要补全标签:`);
needTag.forEach((f, i) => {
console.log(` ${i + 1}. ${f}`);
});
const confirm = await ask(`\n 自动补全这 ${needTag.length} 个文件? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
let success = 0;
let fail = 0;
for (let i = 0; i < needTag.length; i++) {
const f = needTag[i];
const baseName = path.parse(f).name;
console.log(`\n [${i + 1}/${needTag.length}] ${f}`);
// 解析文件名
const match = baseName.match(/^(.+?)\s*[-—]\s*(.+)$/);
if (!match) {
console.log(' ⚠️ 无法解析文件名,跳过');
fail++;
continue;
}
const artist = match[1].trim();
const name = match[2].trim();
try {
// 搜索匹配
const keyword = `${artist} ${name}`;
const results = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=5`);
if (results.code !== 200 || !results.data?.length) {
console.log(' ⚠️ 搜索无结果,跳过');
fail++;
continue;
}
// 找最佳匹配
const best = results.data.find(s => {
const sName = (s.name || '').toLowerCase();
const sArtist = (s.artists || '').toLowerCase();
return sName.includes(name.toLowerCase()) && sArtist.includes(artist.toLowerCase());
}) || results.data[0];
console.log(` ✅ 匹配: ${best.name} - ${best.artists}`);
// 获取完整信息
const songInfo = await fetchJSON(`${API.music}?id=${best.id}&level=standard`);
if (songInfo.code === 200 && songInfo.data) {
const d = songInfo.data;
const filePath = path.join(DOWNLOAD_DIR, f);
// 嵌入封面和标签
if (d.picUrl) {
await embedCover(filePath, d.picUrl, { name: d.name, artist: d.artist, album: d.album });
}
// 获取并保存歌词
const lyricData = await fetchLyric(best.id);
if (lyricData?.lrc) {
const lrcPath = path.join(DOWNLOAD_DIR, baseName + '.lrc');
fs.writeFileSync(lrcPath, lyricData.lrc, 'utf-8');
console.log(' 📄 歌词已保存');
}
// 记录历史
addHistory({ id: best.id, name: d.name, artist: d.artist, album: d.album, level: 'standard', bitrate: d.br, size: d.size, file: f });
success++;
}
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
fail++;
}
if (i < needTag.length - 1) await sleep(500);
}
console.log(`\n 📊 自动补全完成: 成功 ${success} 首, 失败 ${fail}`);
break;
}
case 'z':
case 'Z': {
// 歌单下载并打包 ZIP
const plInput = await ask('请输入歌单ID: ');
const plId = plInput.trim();
if (!/^\d+$/.test(plId)) { console.log('⚠️ 无效歌单ID'); break; }
const pl = await fetchPlaylist(plId);
if (!pl) break;
console.log(`\n📜 歌单: ${pl.name} (${pl.trackCount} 首)`);
level = await askLevel(ask, level);
const confirm = await ask(`确认下载并打包 ${pl.trackCount} 首歌曲? (y/n): `);
if (confirm.trim().toLowerCase() !== 'y') break;
// 创建临时目录
const zipName = sanitize(pl.name || `playlist_${plId}`);
const tempDir = path.join(TEMP_ZIP_DIR, zipName);
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
fs.mkdirSync(tempDir, { recursive: true });
console.log(`\n📥 开始下载到临时目录...\n`);
let downloaded = 0;
let failed = 0;
for (let i = 0; i < pl.tracks.length; i++) {
const t = pl.tracks[i];
const artist = t.ar?.map(a => a.name).join('/') || '未知';
const title = t.name || '未知';
console.log(`[${i + 1}/${pl.tracks.length}] ${title} - ${artist}`);
try {
const result = await fetchJSON(`${API.music}?id=${t.id}&level=${level}`);
if (result.code !== 200 || !result.data?.url) {
console.log(' ❌ 解析失败');
failed++;
continue;
}
const { url: audioUrl, name: sName, artist: sArtist } = result.data;
const ext = audioUrl.match(/\.([a-z0-9]+)(\?|$)/i)?.[1] || 'mp3';
const fileName = sanitize(`${sArtist} - ${sName}.${ext}`);
const dest = path.join(tempDir, fileName);
console.log(` ⬇️ 下载中...`);
await downloadFile(audioUrl, dest, false);
console.log(`${fileName}`);
downloaded++;
} catch (e) {
console.error(` ❌ 出错: ${e.message}`);
failed++;
}
if (i < pl.tracks.length - 1) await sleep(300);
}
if (downloaded === 0) {
console.log('\n❌ 没有歌曲下载成功,取消打包');
fs.rmSync(tempDir, { recursive: true });
break;
}
// 打包 ZIP
const zipPath = path.join(__dirname, `${zipName}.zip`);
console.log(`\n📦 正在打包 ${downloaded} 首歌曲...`);
try {
// 尝试使用系统 zip 命令
execSync(`cd "${path.dirname(tempDir)}" && zip -r "${zipPath}" "${zipName}"`, { stdio: 'ignore' });
console.log(`\n ✅ ZIP 打包完成!`);
console.log(` 📦 文件: ${zipPath}`);
const zipStat = fs.statSync(zipPath);
console.log(` 📊 大小: ${(zipStat.size / 1048576).toFixed(2)} MB`);
console.log(` 📋 包含: ${downloaded} 首歌曲 (失败 ${failed} 首)`);
} catch (e) {
// zip 不可用,尝试 tar.gz
console.log(' ⚠️ zip 命令不可用,尝试 tar.gz...');
const tarPath = path.join(__dirname, `${zipName}.tar.gz`);
try {
execSync(`tar -czf "${tarPath}" -C "${path.dirname(tempDir)}" "${zipName}"`, { stdio: 'ignore' });
console.log(`\n ✅ tar.gz 打包完成!`);
console.log(` 📦 文件: ${tarPath}`);
const tarStat = fs.statSync(tarPath);
console.log(` 📊 大小: ${(tarStat.size / 1048576).toFixed(2)} MB`);
} catch (e2) {
console.error(` ❌ 打包失败: ${e2.message}`);
console.log(` 📁 歌曲已下载到: ${tempDir}`);
console.log(' 请手动打包');
}
}
// 清理临时目录
try { fs.rmSync(tempDir, { recursive: true }); } catch {}
break;
}
case 'c':
case 'C': {
// 清理缓存和临时文件
console.log('\n 🧹 清理缓存和临时文件');
let cleanedSize = 0;
let cleanedCount = 0;
// 清理临时 zip 目录
if (fs.existsSync(TEMP_ZIP_DIR)) {
const tmpSize = getDirSize(TEMP_ZIP_DIR);
fs.rmSync(TEMP_ZIP_DIR, { recursive: true });
cleanedSize += tmpSize;
cleanedCount++;
console.log(` ✅ 已清理临时打包目录 (${(tmpSize / 1024).toFixed(1)} KB)`);
}
// 清理回收站
if (fs.existsSync(TRASH_DIR)) {
const trashFiles = fs.readdirSync(TRASH_DIR);
if (trashFiles.length) {
console.log(`\n 📁 回收站中有 ${trashFiles.length} 个文件`);
const trashConfirm = await ask(' 是否清空回收站? (y/n): ');
if (trashConfirm.trim().toLowerCase() === 'y') {
const trashSize = getDirSize(TRASH_DIR);
fs.rmSync(TRASH_DIR, { recursive: true });
cleanedSize += trashSize;
cleanedCount++;
console.log(` ✅ 回收站已清空 (${(trashSize / 1048576).toFixed(2)} MB)`);
} else {
console.log(' ⏭️ 跳过回收站');
}
} else {
console.log(' 📁 回收站为空');
}
}
// 清理孤立的 .cover.jpg 临时文件
if (fs.existsSync(DOWNLOAD_DIR)) {
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const coverTemps = allFiles.filter(f => f.endsWith('.cover.jpg'));
if (coverTemps.length) {
let coverSize = 0;
for (const f of coverTemps) {
const fp = path.join(DOWNLOAD_DIR, f);
coverSize += fs.statSync(fp).size;
fs.unlinkSync(fp);
}
cleanedSize += coverSize;
cleanedCount++;
console.log(` ✅ 已清理 ${coverTemps.length} 个临时封面文件 (${(coverSize / 1024).toFixed(1)} KB)`);
}
}
// 清理 .tmp 文件
if (fs.existsSync(DOWNLOAD_DIR)) {
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const tmpFiles = allFiles.filter(f => f.includes('.tmp.'));
if (tmpFiles.length) {
let tmpSize = 0;
for (const f of tmpFiles) {
const fp = path.join(DOWNLOAD_DIR, f);
tmpSize += fs.statSync(fp).size;
fs.unlinkSync(fp);
}
cleanedSize += tmpSize;
cleanedCount++;
console.log(` ✅ 已清理 ${tmpFiles.length} 个临时文件 (${(tmpSize / 1024).toFixed(1)} KB)`);
}
}
if (cleanedCount === 0) {
console.log(' ✨ 没有需要清理的文件');
} else {
console.log(`\n 📊 共释放 ${(cleanedSize / 1048576).toFixed(2)} MB 空间`);
}
break;
}
case 'w':
case 'W': {
// 重命名歌曲文件
if (!fs.existsSync(DOWNLOAD_DIR)) {
console.log('\n 📭 下载目录不存在');
break;
}
const allFiles = fs.readdirSync(DOWNLOAD_DIR);
const audioExts = ['.mp3', '.flac', '.m4a', '.wav', '.aac'];
const audioFiles = allFiles.filter(f => audioExts.includes(path.extname(f).toLowerCase()));
if (!audioFiles.length) {
console.log('\n 📭 暂无音频文件');
break;
}
console.log(`\n 📁 已下载的音频文件 (${audioFiles.length} 首):`);
console.log(' ' + '─'.repeat(60));
audioFiles.forEach((f, i) => {
console.log(` ${String(i + 1).padStart(3)} ${f}`);
});
console.log(' ' + '─'.repeat(60));
const pick = await ask('\n 输入要重命名的序号 (如: 1,3,5): ');
if (!pick.trim()) break;
const indices = pick.split(/[,\s]+/)
.map(n => parseInt(n, 10))
.filter(n => n >= 1 && n <= audioFiles.length);
if (!indices.length) { console.log(' ⚠️ 无效序号'); break; }
for (const idx of indices) {
const oldName = audioFiles[idx - 1];
const ext = path.extname(oldName);
const oldBase = path.parse(oldName).name;
console.log(`\n 📝 重命名: ${oldName}`);
console.log(' 格式: 歌手 - 歌名');
// 智能解析现有文件名
const match = oldBase.match(/^(.+?)\s*[-—]\s*(.+)$/);
const defaultArtist = match ? match[1].trim() : '';
const defaultName = match ? match[2].trim() : oldBase;
const newArtist = await ask(` 歌手 [${defaultArtist || '未知'}]: `);
const newName = await ask(` 歌名 [${defaultName}]: `);
const artist = sanitize(newArtist.trim() || defaultArtist || '未知');
const songName = sanitize(newName.trim() || defaultName);
const newBase = `${artist} - ${songName}`;
if (newBase === oldBase) {
console.log(' ⏭️ 名称未变,跳过');
continue;
}
const newFileName = newBase + ext;
const oldPath = path.join(DOWNLOAD_DIR, oldName);
const newPath = path.join(DOWNLOAD_DIR, newFileName);
if (fs.existsSync(newPath)) {
console.log(` ⚠️ 目标文件已存在: ${newFileName},跳过`);
continue;
}
try {
fs.renameSync(oldPath, newPath);
console.log(` ✅ 已重命名: ${newFileName}`);
// 同时重命名关联的 .lrc 文件
const oldLrc = path.join(DOWNLOAD_DIR, oldBase + '.lrc');
const newLrc = path.join(DOWNLOAD_DIR, newBase + '.lrc');
if (fs.existsSync(oldLrc)) {
fs.renameSync(oldLrc, newLrc);
console.log(` ✅ 歌词文件也已重命名`);
}
const oldMergedLrc = path.join(DOWNLOAD_DIR, oldBase + '.合并翻译.lrc');
const newMergedLrc = path.join(DOWNLOAD_DIR, newBase + '.合并翻译.lrc');
if (fs.existsSync(oldMergedLrc)) {
fs.renameSync(oldMergedLrc, newMergedLrc);
console.log(` ✅ 翻译歌词文件也已重命名`);
}
} catch (e) {
console.error(` ❌ 重命名失败: ${e.message}`);
}
}
break;
}
case 't':
case 'T': {
// 下载统计
const history = loadHistory();
if (!history.length) {
console.log('\n 📭 暂无下载记录');
break;
}
const totalSize = history.reduce((sum, h) => sum + (h.size || 0), 0);
const uniqueArtists = new Set(history.map(h => h.artist).filter(Boolean));
const uniqueAlbums = new Set(history.map(h => h.album).filter(Boolean));
const uniqueSongs = new Set(history.map(h => h.id));
// 音质分布
const levelCount = {};
for (const h of history) {
const lv = h.level || '未知';
levelCount[lv] = (levelCount[lv] || 0) + 1;
}
// 歌手 Top 10
const artistCount = {};
for (const h of history) {
if (h.artist) artistCount[h.artist] = (artistCount[h.artist] || 0) + 1;
}
const topArtists = Object.entries(artistCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
// 最近 7 天下载量
const now = Date.now();
const dayMs = 86400000;
const last7days = [];
for (let i = 6; i >= 0; i--) {
const dayStart = new Date(now - i * dayMs);
const dayStr = `${dayStart.getMonth() + 1}/${dayStart.getDate()}`;
const count = history.filter(h => {
const t = new Date(h.timestamp).getTime();
return t >= dayStart.setHours(0,0,0,0) && t < dayStart.setHours(0,0,0,0) + dayMs;
}).length;
last7days.push({ day: dayStr, count });
}
console.log('\n ╔═══════════════════════════════════════╗');
console.log(' ║ 📊 下载统计 ║');
console.log(' ╠═══════════════════════════════════════╣');
console.log(` ║ 总下载次数: ${String(history.length).padStart(6)}`);
console.log(` ║ 去重歌曲数: ${String(uniqueSongs.size).padStart(6)}`);
console.log(` ║ 涉及歌手: ${String(uniqueArtists.size).padStart(6)}`);
console.log(` ║ 涉及专辑: ${String(uniqueAlbums.size).padStart(6)}`);
console.log(` ║ 总文件大小: ${(totalSize / 1073741824).toFixed(2).padStart(6)} GB`);
console.log(' ╠═══════════════════════════════════════╣');
console.log(' ║ 音质分布:');
for (const [lv, cnt] of Object.entries(levelCount).sort((a, b) => b[1] - a[1])) {
const bar = '█'.repeat(Math.min(Math.round(cnt / history.length * 20), 20));
const pct = ((cnt / history.length) * 100).toFixed(0);
console.log(`${lv.padEnd(12)} ${bar} ${cnt}首 (${pct}%)`);
}
if (topArtists.length) {
console.log(' ╠═══════════════════════════════════════╣');
console.log(' ║ 下载最多的歌手:');
for (const [artist, cnt] of topArtists) {
console.log(`${artist.slice(0, 16).padEnd(18)} ${cnt}`);
}
}
console.log(' ╠═══════════════════════════════════════╣');
console.log(' ║ 近 7 天下载量:');
const maxDayCnt = Math.max(...last7days.map(d => d.count), 1);
for (const d of last7days) {
const bar = '▓'.repeat(Math.round(d.count / maxDayCnt * 15));
console.log(`${d.day.padEnd(6)} ${bar || '·'} ${d.count}`);
}
console.log(' ╚═══════════════════════════════════════╝');
break;
}
case 'p':
case 'P': {
// 暂停/恢复
togglePause();
break;
}
case 'r':
case 'R': {
// 重置所有设置
console.log('\n ⚠️ 将重置以下内容:');
console.log(` - 音质等级恢复默认 (${DEFAULT_LEVEL})`);
console.log(' - 清空下载历史');
console.log(' - 删除运行时配置文件');
const confirm = await ask('\n 确认重置? (输入 yes 确认): ');
if (confirm.trim() === 'yes') {
// 重置配置
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
level = DEFAULT_LEVEL;
// 清空历史
const keepHistory = await ask(' 是否保留下载历史? (y/n): ');
if (keepHistory.trim().toLowerCase() !== 'y') {
saveHistory([]);
console.log(' ✅ 下载历史已清空');
}
console.log(` ✅ 音质已恢复默认: ${DEFAULT_LEVEL}`);
console.log(' ✅ 配置已重置');
} else {
console.log(' 取消重置');
}
break;
}
case 'p':
case 'P': {
// 暂停/恢复
togglePause();
break;
}
case 'h':
case 'H': {
// 帮助
console.log(`
╔══════════════════════════════════════════════════════════╗`);
console.log(` ║ 📖 使用帮助 ║`);
console.log(` ╠══════════════════════════════════════════════════════════╣`);
console.log(` ║ ║`);
console.log(` ║ 【搜索与下载】 ║`);
console.log(` ║ 1 搜索歌曲并下载 - 输入关键词搜索,选择后下载 ║`);
console.log(` ║ 2 输入歌曲 ID 下载 - 直接输入网易云歌曲ID ║`);
console.log(` ║ 3 输入歌单 ID 下载全部 - 输入歌单ID批量下载 ║`);
console.log(` ║ s 按歌名搜索 - 按歌名精确搜索,支持试听/详情/下载 ║`);
console.log(` ║ 6 按歌手批量下载 - 搜索歌手并选择下载其歌曲 ║`);
console.log(` ║ a 下载完整专辑 - 输入专辑ID下载全部曲目,也支持名称搜索 ║`);
console.log(` ║ ║`);
console.log(` ║ 【查看与管理】 ║`);
console.log(` ║ 4 查看歌单歌曲列表 - 浏览歌单内容,可选择性下载 ║`);
console.log(` ║ 5 查看歌曲歌词 - 输入ID查看歌词,可保存为.lrc文件 ║`);
console.log(` ║ 7 试听歌曲 - 输入ID在线试听 (需本地播放器) ║`);
console.log(` ║ 8 查看歌曲详情 - 显示音质、大小等详细信息 ║`);
console.log(` ║ l 已下载歌曲列表 - 查看本地已下载的所有歌曲 ║`);
console.log(` ║ ║`);
console.log(` ║ 【数据统计】 ║`);
console.log(` ║ 9 下载历史 - 查看/删除下载记录 ║`);
console.log(` ║ t 下载统计 - 音质分布、歌手排行、近7天趋势 ║`);
console.log(` ║ ║`);
console.log(` ║ 【设置】 ║`);
console.log(` ║ 0 设置音质等级 - 切换下载音质 ║`);
console.log(` ║ ║`);
console.log(` ║ 【命令行模式】 ║`);
console.log(` ║ node 163_music_downloader.js <id1> <id2> ... ║`);
console.log(` ║ node 163_music_downloader.js --playlist=<id> ║`);
console.log(` ║ node 163_music_downloader.js --album=<id> ║`);
console.log(` ║ 可选参数: --level=lossless --retries=5 ║`);
console.log(` ║ --no-lyric --no-cover ║`);
console.log(` ║ ║`);
console.log(` ║ 【音质等级说明】 ║`);
console.log(` ║ standard - 标准 exhigh - 极高 ║`);
console.log(` ║ lossless - 无损 hires - Hi-Res ║`);
console.log(` ║ jymaster - 超清母带 (默认) sky - 空间音频 ║`);
console.log(` ║ jyeffect - 高清臻品 ║`);
console.log(` ║ ║`);
console.log(` ╚══════════════════════════════════════════════════════════╝`);
break;
}
case 'q':
case 'Q':
case 'exit':
case 'quit':
rl.close();
console.log('\n👋 再见!');
process.exit(0);
default:
console.log('⚠️ 无效选项');
}
}
}
main().catch(console.error);