netease-music-downloader
Version:
Download music from NetEase Cloud Music
388 lines (387 loc) • 15.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.proxyConfig = void 0;
exports.setProxy = setProxy;
exports.getSongInfo = getSongInfo;
exports.getAlbumInfo = getAlbumInfo;
exports.checkSongAvailability = checkSongAvailability;
exports.getLyrics = getLyrics;
exports.checkSongAvailabilityWithRetry = checkSongAvailabilityWithRetry;
const axios_1 = __importDefault(require("axios"));
const crypto_1 = require("crypto");
const proxy_1 = require("./proxy");
// 网易云音乐 API 加密参数
const presetKey = '0CoJUm6Qyw8W8jud';
const iv = '0102030405060708';
const eapiKey = 'e82ckenh8dichen8';
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
function setProxy(proxyUrl) {
if (proxyUrl) {
exports.proxyConfig = {
proxy: {
protocol: proxyUrl.startsWith('https') ? 'https' : 'http',
host: new URL(proxyUrl).hostname,
port: parseInt(new URL(proxyUrl).port),
}
};
console.log('代理已设置 Proxy configured:', proxyUrl);
}
else {
exports.proxyConfig = undefined;
}
}
const headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/2.5.1',
'Referer': 'https://music.163.com/',
'Origin': 'https://music.163.com',
'Cookie': 'NMTID=00OJ_vv9oqXwqq8TQFLFUbVeZz059kAAAGMqWD4yw; _ntes_nuid=' + (0, crypto_1.randomBytes)(16).toString('hex'),
};
// 音质等级,按优先级排序
const QUALITY_LEVELS = ['hires', 'lossless', 'exhigh', 'higher', 'standard'];
const QUALITY_BITRATES = {
'standard': '128000',
'higher': '192000',
'exhigh': '320000',
'lossless': '999000',
'hires': '999000'
};
function getRandomString(length) {
let result = '';
for (let i = 0; i < length; i++) {
result += base62[Math.floor(Math.random() * base62.length)];
}
return result;
}
function aesEncrypt(buffer, mode, key, iv) {
const keyBuffer = Buffer.from(key).slice(0, 16);
const ivBuffer = Buffer.from(iv).slice(0, 16);
const cipher = (0, crypto_1.createCipheriv)('aes-128-' + mode, keyBuffer, ivBuffer);
cipher.setAutoPadding(true);
const data = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
return Buffer.concat([cipher.update(data), cipher.final()]);
}
function eapi(url, obj) {
const text = JSON.stringify(obj);
const message = `nobody${url}use${text}md5forencrypt`;
const digest = (0, crypto_1.createHash)('md5').update(message).digest('hex');
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
return {
params: aesEncrypt(data, 'ecb', eapiKey, '').toString('hex').toUpperCase()
};
}
async function downloadImage(url) {
try {
const response = await (0, axios_1.default)({
method: 'get',
url,
responseType: 'arraybuffer',
...exports.proxyConfig
});
return Buffer.from(response.data);
}
catch (error) {
console.error('下载封面图片失败 Failed to download cover image:', error instanceof Error ? error.message : 'Unknown error');
return null;
}
}
async function getSongInfo(id) {
try {
const url = '/api/v3/song/detail';
const data = {
c: JSON.stringify([{ id }]),
header: {
os: 'iOS',
appver: '2.5.1',
deviceId: (0, crypto_1.randomBytes)(8).toString('hex').toUpperCase(),
}
};
const { params } = eapi(url, data);
const response = await axios_1.default.post('https://interface3.music.163.com/eapi/v3/song/detail', new URLSearchParams({
params
}).toString(), {
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)'
},
...exports.proxyConfig
});
const song = response.data?.songs?.[0];
if (!song)
throw new Error('获取歌曲信息失败 Failed to get song info');
const artists = song.ar?.map((artist) => ({
name: artist.name || '未知歌手 Unknown Artist'
})) || [{ name: '未知歌手 Unknown Artist' }];
return {
id: song.id.toString(),
name: `${song.name}${song.alia?.length ? ` (${song.alia[0]})` : ''}`,
artists,
album: {
name: song.al?.name || '',
picUrl: song.al?.picUrl
},
duration: song.dt, // duration in milliseconds
publishTime: song.publishTime
};
}
catch (error) {
console.error('获取歌曲信息失败 Failed to get song info:', error instanceof Error ? error.message : 'Unknown error');
throw error; // 直接抛出错误,而不是返回默认值
}
}
async function getAlbumInfo(albumId) {
try {
const url = '/api/v1/album/' + albumId;
const data = {
header: {
os: 'iOS',
appver: '2.5.1',
deviceId: (0, crypto_1.randomBytes)(8).toString('hex').toUpperCase(),
}
};
const { params } = eapi(url, data);
const response = await axios_1.default.post('https://interface3.music.163.com/eapi/v1/album/' + albumId, new URLSearchParams({
params
}).toString(), {
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)'
}
});
if (response.data?.code !== 200) {
throw new Error(`API 返回错误 API returned error: ${response.data?.message || 'Unknown error'}`);
}
const album = response.data?.album;
if (!album) {
throw new Error('获取专辑信息失败 Failed to get album info');
}
const songs = response.data?.songs || [];
const songList = songs.map((song) => {
const artists = song.ar?.map((artist) => ({
name: artist.name || '未知歌手 Unknown Artist'
})) || [{ name: '未知歌手 Unknown Artist' }];
return {
id: song.id.toString(),
name: `${song.name}${song.alia?.length ? ` (${song.alia[0]})` : ''}`,
artists,
album: {
name: album.name || '',
picUrl: album.picUrl
},
duration: song.dt,
publishTime: song.publishTime
};
});
const albumArtists = album.artists?.map((artist) => artist.name).filter(Boolean) || ['未知歌手 Unknown Artist'];
const albumArtistName = albumArtists.join(',');
return {
songs: songList,
albumName: album.name || '',
artistName: albumArtistName,
picUrl: album.picUrl,
publishTime: album.publishTime
};
}
catch (error) {
console.error('获取专辑信息失败 Failed to get album info:', error instanceof Error ? error.message : 'Unknown error');
return {
songs: [],
albumName: '',
artistName: '未知歌手 Unknown Artist'
};
}
}
async function getSongUrl(id, level) {
try {
const url = '/api/song/enhance/player/url/v1';
const data = {
ids: [id],
level,
encodeType: 'aac',
header: {
os: 'iOS',
appver: '2.5.1',
deviceId: (0, crypto_1.randomBytes)(8).toString('hex').toUpperCase(),
}
};
const { params } = eapi(url, data);
const response = await axios_1.default.post('https://interface3.music.163.com/eapi/song/enhance/player/url/v1', new URLSearchParams({
params
}).toString(), {
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)'
},
timeout: 10000,
...exports.proxyConfig
});
if (response.data?.code !== 200) {
console.error(`API 返回错误 API returned error for ${level}:`, {
code: response.data?.code,
message: response.data?.message,
data: response.data
});
return null;
}
const songData = response.data?.data?.[0];
if (!songData?.url) {
console.log(`未获到 ${level} 音质的 URL No URL found for ${level} quality`);
return null;
}
console.log(`获取到音质 Quality: ${level}, 比特率 Bitrate: ${Math.floor(songData.br / 1000)}kbps, 格式 Format: ${songData.type}, URL: ${songData.url}`);
return songData.url;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
console.error(`尝试获取 ${level} 音质失败 Failed to get ${level} quality:`, {
message: error.message,
response: error.response?.data,
status: error.response?.status,
headers: error.response?.headers
});
}
else {
console.error(`尝试获取 ${level} 音质失败 Failed to get ${level} quality:`, error instanceof Error ? error.message : 'Unknown error');
}
return null;
}
}
async function checkSongAvailability(id) {
// 尝试获取最高音质
for (const level of QUALITY_LEVELS) {
const url = await getSongUrl(id, level);
if (url) {
try {
const response = await axios_1.default.head(url, {
maxRedirects: 5,
validateStatus: status => status >= 200 && status < 400,
headers: {
...headers,
'Referer': 'https://music.163.com/'
},
timeout: 10000,
...exports.proxyConfig
});
const contentLength = parseInt(response.headers['content-length'], 10);
if (contentLength > 500 * 1024) { // 大于 500KB
return {
available: true,
contentLength,
url,
quality: level,
bitrate: Math.floor(contentLength * 8 / (response.headers['content-duration'] || 300) / 1000), // 估算比特率
type: url.split('.').pop()?.split('?')[0]
};
}
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
console.error(`检查音乐可用性失败 Failed to check availability:`, {
message: error.message,
response: error.response?.data,
status: error.response?.status
});
}
else {
console.error(`检查音乐可用性失败 Failed to check availability:`, error instanceof Error ? error.message : 'Unknown error');
}
}
}
}
return { available: false };
}
async function getLyrics(id) {
try {
const url = '/api/song/lyric/v1';
const data = {
id,
lv: 1,
kv: 1,
tv: -1,
header: {
os: 'iOS',
appver: '2.5.1',
deviceId: (0, crypto_1.randomBytes)(8).toString('hex').toUpperCase(),
}
};
const { params } = eapi(url, data);
const apiUrl = 'https://interface3.music.163.com/eapi/song/lyric/v1';
console.log('歌词链接 Lyrics URL:', `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`);
const response = await axios_1.default.post(apiUrl, new URLSearchParams({
params
}).toString(), {
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)'
},
...exports.proxyConfig
});
if (response.data?.code !== 200) {
console.error('获取歌词失败 Failed to get lyrics:', response.data?.message || 'Unknown error');
return null;
}
const lrc = response.data?.lrc?.lyric;
if (!lrc) {
console.log('该歌曲无歌词 No lyrics available for this song');
return null;
}
return lrc;
}
catch (error) {
console.error('获取歌词失败 Failed to get lyrics:', error instanceof Error ? error.message : 'Unknown error');
return null;
}
}
async function checkSongAvailabilityWithRetry(id, autoProxy) {
// 先尝试直连
console.log('尝试直连下载 Trying direct connection...');
const originalProxy = exports.proxyConfig;
setProxy(undefined);
try {
const result = await checkSongAvailability(id);
if (result.available) {
console.log('直连成功 Direct connection successful');
return { ...result, needProxy: false };
}
}
catch (error) {
console.log('直连失败 Direct connection failed');
}
// 如果直连失败且启用了自动代理,尝试寻找可用代理
if (autoProxy) {
console.log('正在寻找可用的代理服务器 Finding available proxy server...');
const proxyUrl = await (0, proxy_1.getAutoProxy)();
if (proxyUrl) {
try {
const result = await checkSongAvailability(id);
return { ...result, needProxy: true };
}
catch (error) {
console.log('代理连接也失败了 Proxy connection also failed');
}
}
else {
console.log('未找到可用的代理服务器 No available proxy found');
}
}
// 如果有预设的代理配置,尝试使用
else if (originalProxy?.proxy && typeof originalProxy.proxy !== 'boolean') {
console.log('尝试使用预设代理 Trying with preset proxy...');
const proxyUrl = `${originalProxy.proxy.protocol}://${originalProxy.proxy.host}:${originalProxy.proxy.port}`;
setProxy(proxyUrl);
try {
const result = await checkSongAvailability(id);
return { ...result, needProxy: true };
}
catch (error) {
console.log('代理连接也失败了 Proxy connection also failed');
}
}
return { available: false, needProxy: false };
}