Files
163-music-dl/lib/downloader.js
T

145 lines
5.7 KiB
JavaScript
Executable File
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 { API, DEFAULT_RETRIES, DOWNLOAD_DIR } = require('./config');
const { addHistory } = require('./history');
const { fetchLyric, mergeLrc, parseCredits } = require('./lyrics');
const { fetchNeteaseAlbumDetail, fetchNeteaseSongDetail } = require('./metadata');
const { downloadFile, fetchJSON } = require('./network');
const { embedCover } = require('./tagging');
const { sanitize, formatPersonNames, sleep } = require('./utils');
async function downloadSong(songId, level, {
downloadLyric = true,
embedCoverArt = true,
maxRetries = DEFAULT_RETRIES,
outputDir = DOWNLOAD_DIR,
outputBaseName = null,
} = {}) {
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await downloadSongOnce(songId, level, { downloadLyric, embedCoverArt, outputDir, outputBaseName });
} catch (e) {
lastError = e;
if (attempt < maxRetries) {
const waitSec = attempt * 3;
console.log(` ⚠️ 第 ${attempt} 次失败: ${e.message}${waitSec}秒后重试...`);
await sleep(waitSec * 1000);
}
}
}
console.error(`❌ 歌曲 ${songId} 下载失败 (已重试 ${maxRetries} 次): ${lastError?.message}`);
return false;
}
async function downloadSongOnce(songId, level, {
downloadLyric = true,
embedCoverArt = true,
outputDir = DOWNLOAD_DIR,
outputBaseName = null,
} = {}) {
console.log(`\n🎵 解析歌曲 ID: ${songId} ...`);
const result = await fetchJSON(`${API.music}?id=${songId}&level=${level}`);
if (result.code !== 200 || !result.data?.url) throw new Error(result.msg || 'API 返回错误');
const { name, artist: rawArtist, album, url: audioUrl, br, size, level: actualLevel, picUrl } = result.data;
const artist = formatPersonNames(rawArtist);
const ext = audioUrl.match(/\.(mp3|flac|m4a|wav|aac)(\?|$)/i)?.[1] || 'mp3';
const baseName = sanitize(outputBaseName || `${artist} - ${name}`);
const fileName = `${baseName}.${ext}`;
fs.mkdirSync(outputDir, { recursive: true });
const dest = path.join(outputDir, fileName);
console.log(` 🎶 ${name} - ${artist}`);
console.log(` 💿 ${album} | ${actualLevel} (${br}kbps) | ${(size / 1048576).toFixed(2)} MB`);
if (fs.existsSync(dest)) {
const existStat = fs.statSync(dest);
const sizeDiff = Math.abs(existStat.size - size) / size;
if (sizeDiff < 0.01) {
console.log(` ⏭️ 已存在,跳过: ${fileName}`);
const lrcPath = path.join(outputDir, baseName + '.lrc');
if (downloadLyric && !fs.existsSync(lrcPath)) {
console.log(' 🔧 检测到缺少歌词,自动补全...');
const lyricData = await fetchLyric(songId);
if (lyricData?.lrc) {
fs.writeFileSync(lrcPath, lyricData.lrc, 'utf-8');
console.log(' 📄 歌词已补全');
if (lyricData.tlyric?.trim()) {
fs.writeFileSync(path.join(outputDir, baseName + '.合并翻译.lrc'), mergeLrc(lyricData.lrc, lyricData.tlyric), 'utf-8');
}
}
}
} else {
console.log(` ⚠️ 已存在同名文件但大小不同 (现有 ${(existStat.size / 1048576).toFixed(2)} MB, 预期 ${(size / 1048576).toFixed(2)} MB)`);
console.log(' ⬇️ 覆盖下载...');
await downloadFile(audioUrl, dest);
console.log(` ✅ 下载完成: ${fileName}`);
}
} else {
console.log(' ⬇️ 下载中...');
await downloadFile(audioUrl, dest);
console.log(` ✅ 下载完成: ${fileName}`);
}
let lyricData = null;
if (downloadLyric) {
lyricData = await fetchLyric(songId);
if (lyricData?.lrc) {
fs.writeFileSync(path.join(outputDir, baseName + '.lrc'), lyricData.lrc, 'utf-8');
console.log(` 📄 歌词已保存: ${baseName}.lrc`);
if (lyricData.tlyric?.trim()) {
const mergedPath = path.join(outputDir, baseName + '.合并翻译.lrc');
fs.writeFileSync(mergedPath, mergeLrc(lyricData.lrc, lyricData.tlyric), 'utf-8');
console.log(` 📄 翻译歌词已保存: ${path.basename(mergedPath)}`);
}
} else {
console.log(' ⚠️ 未获取到歌词');
}
}
if (embedCoverArt && picUrl) {
const lrcText = lyricData?.lrc || null;
const tagMeta = { name, artist, album, lyrics: lrcText, ...parseCredits(lrcText) };
try {
const detail = await fetchNeteaseSongDetail(songId);
if (detail) {
if (detail.trackNo) tagMeta.trackNo = detail.trackNo;
if (detail.disc) tagMeta.disc = detail.disc;
if (detail.album?.id) {
const albumDetail = await fetchNeteaseAlbumDetail(detail.album.id);
if (albumDetail?.albumArtist) tagMeta.albumArtist = albumDetail.albumArtist;
if (albumDetail?.isCompilation) tagMeta.compilation = true;
if (albumDetail?.publishTime) {
const date = new Date(albumDetail.publishTime);
tagMeta.year = date.getFullYear();
tagMeta.date = date.toISOString().slice(0, 10);
}
if (albumDetail?.tags?.length) tagMeta.genre = albumDetail.tags.join(', ');
if (albumDetail?.company) tagMeta.publisher = albumDetail.company;
if (tagMeta.trackNo && albumDetail?.size) tagMeta.trackNo = `${tagMeta.trackNo}/${albumDetail.size}`;
}
if (!tagMeta.albumArtist) {
const fallbackAlbumArtist = detail.artists?.map(item => item.name).join(' / ');
if (fallbackAlbumArtist) tagMeta.albumArtist = fallbackAlbumArtist;
}
}
} catch {}
await embedCover(dest, picUrl, tagMeta);
}
addHistory({
id: songId,
name,
artist,
album,
level: actualLevel,
bitrate: br,
size,
file: path.relative(DOWNLOAD_DIR, dest),
});
return true;
}
module.exports = { downloadSong };