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 ... ║`); console.log(` ║ node 163_music_downloader.js --playlist= ║`); console.log(` ║ node 163_music_downloader.js --album= ║`); 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);