feat: migrate ChKSz API and support API key authentication
This commit is contained in:
+19
-18
@@ -14,6 +14,7 @@ const {
|
|||||||
DEFAULT_RETRIES,
|
DEFAULT_RETRIES,
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
|
buildApiUrl,
|
||||||
} = require('./lib/config');
|
} = require('./lib/config');
|
||||||
const { fetchJSON, downloadFile } = require('./lib/network');
|
const { fetchJSON, downloadFile } = require('./lib/network');
|
||||||
const { sanitize, sleep, getDirSize } = require('./lib/utils');
|
const { sanitize, sleep, getDirSize } = require('./lib/utils');
|
||||||
@@ -479,7 +480,7 @@ async function main() {
|
|||||||
|
|
||||||
console.log(`\n🔍 获取试听链接 (ID: ${songId}) ...`);
|
console.log(`\n🔍 获取试听链接 (ID: ${songId}) ...`);
|
||||||
try {
|
try {
|
||||||
const result = await fetchJSON(`${API.music}?id=${songId}&level=standard`);
|
const result = await fetchJSON(buildApiUrl(API.music, { id: songId, level: 'standard' }));
|
||||||
if (result.code !== 200 || !result.data?.url) {
|
if (result.code !== 200 || !result.data?.url) {
|
||||||
console.log(`❌ 获取失败: ${result.msg || '未知错误'}`);
|
console.log(`❌ 获取失败: ${result.msg || '未知错误'}`);
|
||||||
break;
|
break;
|
||||||
@@ -729,7 +730,7 @@ async function main() {
|
|||||||
const s = results[idx - 1];
|
const s = results[idx - 1];
|
||||||
console.log(`\n ▶️ 试听: ${s.name} - ${s.artists}`);
|
console.log(`\n ▶️ 试听: ${s.name} - ${s.artists}`);
|
||||||
try {
|
try {
|
||||||
const r = await fetchJSON(`${API.music}?id=${s.id}&level=standard`);
|
const r = await fetchJSON(buildApiUrl(API.music, { id: s.id, level: 'standard' }));
|
||||||
if (r.code === 200 && r.data?.url) {
|
if (r.code === 200 && r.data?.url) {
|
||||||
const players = [
|
const players = [
|
||||||
{ cmd: 'mpv', args: (u) => ['--no-video', u] },
|
{ cmd: 'mpv', args: (u) => ['--no-video', u] },
|
||||||
@@ -763,7 +764,7 @@ async function main() {
|
|||||||
if (idx >= 1 && idx <= results.length) {
|
if (idx >= 1 && idx <= results.length) {
|
||||||
const s = results[idx - 1];
|
const s = results[idx - 1];
|
||||||
try {
|
try {
|
||||||
const r = await fetchJSON(`${API.music}?id=${s.id}&level=jymaster`);
|
const r = await fetchJSON(buildApiUrl(API.music, { id: s.id, level: 'jymaster' }));
|
||||||
if (r.code === 200 && r.data) {
|
if (r.code === 200 && r.data) {
|
||||||
const d = r.data;
|
const d = r.data;
|
||||||
console.log('\n ┌─────────────────────────────────────────┐');
|
console.log('\n ┌─────────────────────────────────────────┐');
|
||||||
@@ -1234,9 +1235,9 @@ async function main() {
|
|||||||
const parts = nameLine.split(/\s*[-—]\s*/);
|
const parts = nameLine.split(/\s*[-—]\s*/);
|
||||||
const keyword = parts.length >= 2 ? `${parts[0]} ${parts[1]}` : nameLine;
|
const keyword = parts.length >= 2 ? `${parts[0]} ${parts[1]}` : nameLine;
|
||||||
try {
|
try {
|
||||||
const res = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=3`);
|
const songs = await searchSongs(keyword, 3);
|
||||||
if (res.code === 200 && res.data?.length) {
|
if (songs.length) {
|
||||||
const best = res.data[0];
|
const best = songs[0];
|
||||||
resolvedIds.push(best.id);
|
resolvedIds.push(best.id);
|
||||||
console.log(` ✅ "${nameLine}" → ${best.name} - ${best.artists} (ID: ${best.id})`);
|
console.log(` ✅ "${nameLine}" → ${best.name} - ${best.artists} (ID: ${best.id})`);
|
||||||
} else {
|
} else {
|
||||||
@@ -1357,24 +1358,24 @@ async function main() {
|
|||||||
try {
|
try {
|
||||||
// 搜索匹配
|
// 搜索匹配
|
||||||
const keyword = `${c.artist} ${c.name}`;
|
const keyword = `${c.artist} ${c.name}`;
|
||||||
const results = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=5`);
|
const results = await searchSongs(keyword, 5);
|
||||||
if (results.code !== 200 || !results.data?.length) {
|
if (!results.length) {
|
||||||
console.log(' ⚠️ 未找到匹配');
|
console.log(' ⚠️ 未找到匹配');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找最佳匹配
|
// 找最佳匹配
|
||||||
const best = results.data.find(s => {
|
const best = results.find(s => {
|
||||||
const sName = (s.name || '').toLowerCase();
|
const sName = (s.name || '').toLowerCase();
|
||||||
const sArtist = (s.artists || '').toLowerCase();
|
const sArtist = (s.artists || '').toLowerCase();
|
||||||
return sName.includes(c.name.toLowerCase()) && sArtist.includes(c.artist.toLowerCase());
|
return sName.includes(c.name.toLowerCase()) && sArtist.includes(c.artist.toLowerCase());
|
||||||
}) || results.data[0];
|
}) || results[0];
|
||||||
|
|
||||||
console.log(` ✅ 匹配: ${best.name} - ${best.artists} (ID: ${best.id})`);
|
console.log(` ✅ 匹配: ${best.name} - ${best.artists} (ID: ${best.id})`);
|
||||||
|
|
||||||
// 获取完整歌曲信息并嵌入标签
|
// 获取完整歌曲信息并嵌入标签
|
||||||
const filePath = path.join(DOWNLOAD_DIR, c.file);
|
const filePath = path.join(DOWNLOAD_DIR, c.file);
|
||||||
const songInfo = await fetchJSON(`${API.music}?id=${best.id}&level=standard`);
|
const songInfo = await fetchJSON(buildApiUrl(API.music, { id: best.id, level: 'standard' }));
|
||||||
if (songInfo.code === 200 && songInfo.data) {
|
if (songInfo.code === 200 && songInfo.data) {
|
||||||
const d = songInfo.data;
|
const d = songInfo.data;
|
||||||
if (d.picUrl) {
|
if (d.picUrl) {
|
||||||
@@ -1523,7 +1524,7 @@ async function main() {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
// 用 API 链接作为备选
|
// 用 API 链接作为备选
|
||||||
m3uLines.push(`#EXTVLCOPT:network-caching=1000`);
|
m3uLines.push(`#EXTVLCOPT:network-caching=1000`);
|
||||||
m3uLines.push(`${API.music}?id=${t.id}&type=down`);
|
m3uLines.push(buildApiUrl(API.music, { id: t.id, type: 'down' }));
|
||||||
}
|
}
|
||||||
m3uLines.push('');
|
m3uLines.push('');
|
||||||
}
|
}
|
||||||
@@ -1615,24 +1616,24 @@ async function main() {
|
|||||||
try {
|
try {
|
||||||
// 搜索匹配
|
// 搜索匹配
|
||||||
const keyword = `${artist} ${name}`;
|
const keyword = `${artist} ${name}`;
|
||||||
const results = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=5`);
|
const results = await searchSongs(keyword, 5);
|
||||||
if (results.code !== 200 || !results.data?.length) {
|
if (!results.length) {
|
||||||
console.log(' ⚠️ 搜索无结果,跳过');
|
console.log(' ⚠️ 搜索无结果,跳过');
|
||||||
fail++;
|
fail++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找最佳匹配
|
// 找最佳匹配
|
||||||
const best = results.data.find(s => {
|
const best = results.find(s => {
|
||||||
const sName = (s.name || '').toLowerCase();
|
const sName = (s.name || '').toLowerCase();
|
||||||
const sArtist = (s.artists || '').toLowerCase();
|
const sArtist = (s.artists || '').toLowerCase();
|
||||||
return sName.includes(name.toLowerCase()) && sArtist.includes(artist.toLowerCase());
|
return sName.includes(name.toLowerCase()) && sArtist.includes(artist.toLowerCase());
|
||||||
}) || results.data[0];
|
}) || results[0];
|
||||||
|
|
||||||
console.log(` ✅ 匹配: ${best.name} - ${best.artists}`);
|
console.log(` ✅ 匹配: ${best.name} - ${best.artists}`);
|
||||||
|
|
||||||
// 获取完整信息
|
// 获取完整信息
|
||||||
const songInfo = await fetchJSON(`${API.music}?id=${best.id}&level=standard`);
|
const songInfo = await fetchJSON(buildApiUrl(API.music, { id: best.id, level: 'standard' }));
|
||||||
if (songInfo.code === 200 && songInfo.data) {
|
if (songInfo.code === 200 && songInfo.data) {
|
||||||
const d = songInfo.data;
|
const d = songInfo.data;
|
||||||
const filePath = path.join(DOWNLOAD_DIR, f);
|
const filePath = path.join(DOWNLOAD_DIR, f);
|
||||||
@@ -1698,7 +1699,7 @@ async function main() {
|
|||||||
console.log(`[${i + 1}/${pl.tracks.length}] ${title} - ${artist}`);
|
console.log(`[${i + 1}/${pl.tracks.length}] ${title} - ${artist}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fetchJSON(`${API.music}?id=${t.id}&level=${level}`);
|
const result = await fetchJSON(buildApiUrl(API.music, { id: t.id, level }));
|
||||||
if (result.code !== 200 || !result.data?.url) {
|
if (result.code !== 200 || !result.data?.url) {
|
||||||
console.log(' ❌ 解析失败');
|
console.log(' ❌ 解析失败');
|
||||||
failed++;
|
failed++;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ There are no tests, linter, or build step. Verify changes by running the script.
|
|||||||
|
|
||||||
## External dependencies (all optional / runtime-detected)
|
## External dependencies (all optional / runtime-detected)
|
||||||
|
|
||||||
- **`api.chksz.top`** — third-party API (`API` object at top of file) for song URLs, search, lyrics, and playlists. This is the core download source.
|
- **`api.chksz.com`** — third-party API (`API` object in `lib/config.js`) for song URLs, search, lyrics, and playlists (requires `apikey`). This is the core download source.
|
||||||
- **`music.163.com`** — queried directly (`fetchNeteaseSongDetail`, `fetchNeteaseAlbumDetail`) for rich metadata (album, publish date, popularity). Requires `Cookie: os=pc` header.
|
- **`music.163.com`** — queried directly (`fetchNeteaseSongDetail`, `fetchNeteaseAlbumDetail`) for rich metadata (album, publish date, popularity). Requires `Cookie: os=pc` header.
|
||||||
- **`node-id3`** (npm) — preferred for MP3 tag/lyric/cover embedding. `require`d lazily via `hasNodeID3()`; absent by default.
|
- **`node-id3`** (npm) — preferred for MP3 tag/lyric/cover embedding. `require`d lazily via `hasNodeID3()`; absent by default.
|
||||||
- **`ffmpeg`** (system) — fallback embedder for non-MP3 (FLAC etc.) and when node-id3 is missing. Detected via `hasFFmpeg()`.
|
- **`ffmpeg`** (system) — fallback embedder for non-MP3 (FLAC etc.) and when node-id3 is missing. Detected via `hasFFmpeg()`.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ There are no tests, linter, or build step. Verify changes by running the script.
|
|||||||
|
|
||||||
## External dependencies (all optional / runtime-detected)
|
## External dependencies (all optional / runtime-detected)
|
||||||
|
|
||||||
- **`api.chksz.top`** — third-party API (`API` object at top of file) for song URLs, search, lyrics, and playlists. This is the core download source.
|
- **`api.chksz.com`** — third-party API (`API` object in `lib/config.js`) for song URLs, search, lyrics, and playlists (requires `apikey`). This is the core download source.
|
||||||
- **`music.163.com`** — queried directly (`fetchNeteaseSongDetail`, `fetchNeteaseAlbumDetail`) for rich metadata (album, publish date, popularity). Requires `Cookie: os=pc` header.
|
- **`music.163.com`** — queried directly (`fetchNeteaseSongDetail`, `fetchNeteaseAlbumDetail`) for rich metadata (album, publish date, popularity). Requires `Cookie: os=pc` header.
|
||||||
- **`node-id3`** (npm) — preferred for MP3 tag/lyric/cover embedding. `require`d lazily via `hasNodeID3()`; absent by default.
|
- **`node-id3`** (npm) — preferred for MP3 tag/lyric/cover embedding. `require`d lazily via `hasNodeID3()`; absent by default.
|
||||||
- **`ffmpeg`** (system) — fallback embedder for non-MP3 (FLAC etc.) and when node-id3 is missing. Detected via `hasFFmpeg()`.
|
- **`ffmpeg`** (system) — fallback embedder for non-MP3 (FLAC etc.) and when node-id3 is missing. Detected via `hasFFmpeg()`.
|
||||||
|
|||||||
@@ -115,10 +115,11 @@ node 163_music_downloader.js 347230 --level=hires --no-cover
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"music": "https://api.chksz.top/api/163_music",
|
"apikey": "YOUR_API_KEY",
|
||||||
"lyric": "https://api.chksz.top/api/163_lyric",
|
"music": "https://api.chksz.com/api/163_music",
|
||||||
"search": "https://api.chksz.top/api/163_search",
|
"lyric": "https://api.chksz.com/api/163_lyric",
|
||||||
"playlist": "https://api.chksz.top/api/163_playlist",
|
"search": "https://api.chksz.com/api/163_search",
|
||||||
|
"playlist": "https://api.chksz.com/api/163_playlist",
|
||||||
"neteaseBaseUrl": "https://music.163.com"
|
"neteaseBaseUrl": "https://music.163.com"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -151,7 +152,7 @@ node 163_music_downloader.js 347230 --level=hires --no-cover
|
|||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 下载能力依赖第三方 `api.chksz.top` 接口,其可用性、音源和返回质量不由本项目保证。
|
- 下载能力依赖第三方 `api.chksz.com` 接口(需配置 `apikey`),其可用性、音源和返回质量不由本项目保证。
|
||||||
- 歌曲详情和专辑元数据会直接请求网易云音乐。
|
- 歌曲详情和专辑元数据会直接请求网易云音乐。
|
||||||
- 请遵守当地法律法规、平台条款和音乐版权要求,仅将本工具用于合法用途。
|
- 请遵守当地法律法规、平台条款和音乐版权要求,仅将本工具用于合法用途。
|
||||||
- 音频文件和压缩包已通过 `.gitignore` 排除,不会被提交到仓库。
|
- 音频文件和压缩包已通过 `.gitignore` 排除,不会被提交到仓库。
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ node 163_music_downloader.js --album=专辑ID
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"music": "https://api.chksz.top/api/163_music",
|
"apikey": "YOUR_API_KEY",
|
||||||
"lyric": "https://api.chksz.top/api/163_lyric",
|
"music": "https://api.chksz.com/api/163_music",
|
||||||
"search": "https://api.chksz.top/api/163_search",
|
"lyric": "https://api.chksz.com/api/163_lyric",
|
||||||
"playlist": "https://api.chksz.top/api/163_playlist",
|
"search": "https://api.chksz.com/api/163_search",
|
||||||
|
"playlist": "https://api.chksz.com/api/163_playlist",
|
||||||
"neteaseBaseUrl": "https://music.163.com"
|
"neteaseBaseUrl": "https://music.163.com"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -72,6 +73,7 @@ node 163_music_downloader.js --album=专辑ID
|
|||||||
|
|
||||||
按以下规则设置:
|
按以下规则设置:
|
||||||
|
|
||||||
|
- 使用 `api.apikey` 设置 ChKSz API 个人鉴权密钥。
|
||||||
- 使用 `api.music` 获取音频地址和基础歌曲信息。
|
- 使用 `api.music` 获取音频地址和基础歌曲信息。
|
||||||
- 使用 `api.lyric` 获取原文和翻译歌词。
|
- 使用 `api.lyric` 获取原文和翻译歌词。
|
||||||
- 使用 `api.search` 搜索歌曲、歌手和专辑。
|
- 使用 `api.search` 搜索歌曲、歌手和专辑。
|
||||||
|
|||||||
+5
-4
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"music": "https://api.chksz.top/api/163_music",
|
"apikey": "",
|
||||||
"lyric": "https://api.chksz.top/api/163_lyric",
|
"music": "https://api.chksz.com/api/163_music",
|
||||||
"search": "https://api.chksz.top/api/163_search",
|
"lyric": "https://api.chksz.com/api/163_lyric",
|
||||||
"playlist": "https://api.chksz.top/api/163_playlist",
|
"search": "https://api.chksz.com/api/163_search",
|
||||||
|
"playlist": "https://api.chksz.com/api/163_playlist",
|
||||||
"neteaseBaseUrl": "https://music.163.com"
|
"neteaseBaseUrl": "https://music.163.com"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
const { API } = require('./config');
|
const { API, buildApiUrl } = require('./config');
|
||||||
const { fetchJSON } = require('./network');
|
const { fetchJSON } = require('./network');
|
||||||
const { formatPersonNames } = require('./utils');
|
const { formatPersonNames } = require('./utils');
|
||||||
|
|
||||||
async function searchSongs(keyword, limit = 10) {
|
async function searchSongs(keyword, limit = 10) {
|
||||||
const res = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=${limit}`);
|
const res = await fetchJSON(buildApiUrl(API.search, { keyword, limit }));
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
console.log('❌ 搜索无结果');
|
console.log(`❌ 搜索失败: ${res.msg || '无结果'}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const songs = Array.isArray(res.data) ? res.data : (res.data?.songs || []);
|
const songs = Array.isArray(res.data) ? res.data : (res.data?.songs || []);
|
||||||
@@ -40,9 +40,9 @@ async function interactiveSearch(rl, ask) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPlaylist(playlistId) {
|
async function fetchPlaylist(playlistId) {
|
||||||
const res = await fetchJSON(`${API.playlist}?id=${playlistId}`);
|
const res = await fetchJSON(buildApiUrl(API.playlist, { id: playlistId }));
|
||||||
if (!res.data?.tracks?.length) {
|
if (!res.data?.tracks?.length) {
|
||||||
console.log('❌ 歌单获取失败或为空');
|
console.log(`❌ 歌单获取失败或为空${res.msg ? `: ${res.msg}` : ''}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
+20
-4
@@ -5,10 +5,11 @@ const ROOT_DIR = path.join(__dirname, '..');
|
|||||||
const APP_CONFIG_FILE = path.join(ROOT_DIR, 'config.json');
|
const APP_CONFIG_FILE = path.join(ROOT_DIR, 'config.json');
|
||||||
const DEFAULT_APP_CONFIG = {
|
const DEFAULT_APP_CONFIG = {
|
||||||
api: {
|
api: {
|
||||||
music: 'https://api.chksz.top/api/163_music',
|
apikey: process.env.CHKSZ_API_KEY || '',
|
||||||
lyric: 'https://api.chksz.top/api/163_lyric',
|
music: 'https://api.chksz.com/api/163_music',
|
||||||
search: 'https://api.chksz.top/api/163_search',
|
lyric: 'https://api.chksz.com/api/163_lyric',
|
||||||
playlist: 'https://api.chksz.top/api/163_playlist',
|
search: 'https://api.chksz.com/api/163_search',
|
||||||
|
playlist: 'https://api.chksz.com/api/163_playlist',
|
||||||
neteaseBaseUrl: 'https://music.163.com',
|
neteaseBaseUrl: 'https://music.163.com',
|
||||||
},
|
},
|
||||||
paths: {
|
paths: {
|
||||||
@@ -51,12 +52,26 @@ function getConfigString(value, fallback) {
|
|||||||
|
|
||||||
const APP_CONFIG = loadAppConfig();
|
const APP_CONFIG = loadAppConfig();
|
||||||
const API = {
|
const API = {
|
||||||
|
apikey: getConfigString(APP_CONFIG.api.apikey, DEFAULT_APP_CONFIG.api.apikey),
|
||||||
music: getConfigString(APP_CONFIG.api.music, DEFAULT_APP_CONFIG.api.music),
|
music: getConfigString(APP_CONFIG.api.music, DEFAULT_APP_CONFIG.api.music),
|
||||||
lyric: getConfigString(APP_CONFIG.api.lyric, DEFAULT_APP_CONFIG.api.lyric),
|
lyric: getConfigString(APP_CONFIG.api.lyric, DEFAULT_APP_CONFIG.api.lyric),
|
||||||
search: getConfigString(APP_CONFIG.api.search, DEFAULT_APP_CONFIG.api.search),
|
search: getConfigString(APP_CONFIG.api.search, DEFAULT_APP_CONFIG.api.search),
|
||||||
playlist: getConfigString(APP_CONFIG.api.playlist, DEFAULT_APP_CONFIG.api.playlist),
|
playlist: getConfigString(APP_CONFIG.api.playlist, DEFAULT_APP_CONFIG.api.playlist),
|
||||||
neteaseBaseUrl: getConfigString(APP_CONFIG.api.neteaseBaseUrl, DEFAULT_APP_CONFIG.api.neteaseBaseUrl),
|
neteaseBaseUrl: getConfigString(APP_CONFIG.api.neteaseBaseUrl, DEFAULT_APP_CONFIG.api.neteaseBaseUrl),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildApiUrl(endpoint, params = {}) {
|
||||||
|
const url = new URL(endpoint);
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (API.apikey && !url.searchParams.has('apikey')) {
|
||||||
|
url.searchParams.set('apikey', API.apikey);
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
const NETEASE_BASE_URL = API.neteaseBaseUrl.replace(/\/$/, '');
|
const NETEASE_BASE_URL = API.neteaseBaseUrl.replace(/\/$/, '');
|
||||||
const DOWNLOAD_DIR = resolveConfigPath(APP_CONFIG.paths.downloadDir, DEFAULT_APP_CONFIG.paths.downloadDir);
|
const DOWNLOAD_DIR = resolveConfigPath(APP_CONFIG.paths.downloadDir, DEFAULT_APP_CONFIG.paths.downloadDir);
|
||||||
const HISTORY_FILE = resolveConfigPath(APP_CONFIG.paths.historyFile, DEFAULT_APP_CONFIG.paths.historyFile);
|
const HISTORY_FILE = resolveConfigPath(APP_CONFIG.paths.historyFile, DEFAULT_APP_CONFIG.paths.historyFile);
|
||||||
@@ -123,4 +138,5 @@ module.exports = {
|
|||||||
HISTORY_LIMIT,
|
HISTORY_LIMIT,
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
|
buildApiUrl,
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { API, DEFAULT_RETRIES, DOWNLOAD_DIR } = require('./config');
|
const { API, DEFAULT_RETRIES, DOWNLOAD_DIR, buildApiUrl } = require('./config');
|
||||||
const { addHistory } = require('./history');
|
const { addHistory } = require('./history');
|
||||||
const { fetchLyric, mergeLrc, parseCredits } = require('./lyrics');
|
const { fetchLyric, mergeLrc, parseCredits } = require('./lyrics');
|
||||||
const { fetchNeteaseAlbumDetail, fetchNeteaseSongDetail } = require('./metadata');
|
const { fetchNeteaseAlbumDetail, fetchNeteaseSongDetail } = require('./metadata');
|
||||||
@@ -39,7 +39,7 @@ async function downloadSongOnce(songId, level, {
|
|||||||
outputBaseName = null,
|
outputBaseName = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
console.log(`\n🎵 解析歌曲 ID: ${songId} ...`);
|
console.log(`\n🎵 解析歌曲 ID: ${songId} ...`);
|
||||||
const result = await fetchJSON(`${API.music}?id=${songId}&level=${level}`);
|
const result = await fetchJSON(buildApiUrl(API.music, { id: songId, level }));
|
||||||
if (result.code !== 200 || !result.data?.url) throw new Error(result.msg || 'API 返回错误');
|
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 { name, artist: rawArtist, album, url: audioUrl, br, size, level: actualLevel, picUrl } = result.data;
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { API, DOWNLOAD_DIR } = require('./config');
|
const { API, DOWNLOAD_DIR, buildApiUrl } = require('./config');
|
||||||
const { fetchJSON } = require('./network');
|
const { fetchJSON } = require('./network');
|
||||||
const { formatPersonNames } = require('./utils');
|
const { formatPersonNames } = require('./utils');
|
||||||
|
|
||||||
async function fetchLyric(songId) {
|
async function fetchLyric(songId) {
|
||||||
try {
|
try {
|
||||||
const res = await fetchJSON(`${API.lyric}?id=${songId}`);
|
const res = await fetchJSON(buildApiUrl(API.lyric, { id: songId }));
|
||||||
if (res.code !== 200) return null;
|
if (res.code !== 200) return null;
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { API, DOWNLOAD_DIR, NETEASE_BASE_URL } = require('./config');
|
const { API, DOWNLOAD_DIR, NETEASE_BASE_URL, buildApiUrl } = require('./config');
|
||||||
const { downloadBuffer, fetchJSON } = require('./network');
|
const { downloadBuffer, fetchJSON } = require('./network');
|
||||||
const { sanitizePathSegment } = require('./utils');
|
const { sanitizePathSegment } = require('./utils');
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ async function saveAlbumCover(coverUrl, outputDir) {
|
|||||||
async function fetchFullMetadata(songId) {
|
async function fetchFullMetadata(songId) {
|
||||||
const [songDetail, chkszData] = await Promise.all([
|
const [songDetail, chkszData] = await Promise.all([
|
||||||
fetchNeteaseSongDetail(songId),
|
fetchNeteaseSongDetail(songId),
|
||||||
fetchJSON(`${API.music}?id=${songId}&level=standard`).catch(() => null),
|
fetchJSON(buildApiUrl(API.music, { id: songId, level: 'standard' })).catch(() => null),
|
||||||
]);
|
]);
|
||||||
if (!songDetail) return null;
|
if (!songDetail) return null;
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -7,8 +7,20 @@ function getProtocol(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchJSON(url) {
|
function fetchJSON(url) {
|
||||||
|
let targetUrl = url;
|
||||||
|
try {
|
||||||
|
const { API } = require('./config');
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (!parsed.searchParams.has('apikey') && API?.apikey && parsed.hostname.includes('chksz.')) {
|
||||||
|
parsed.searchParams.set('apikey', API.apikey);
|
||||||
|
targetUrl = parsed.toString();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
getProtocol(url).get(url, (res) => {
|
getProtocol(targetUrl).get(targetUrl, (res) => {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
return fetchJSON(res.headers.location).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', chunk => data += chunk);
|
res.on('data', chunk => data += chunk);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
|
|||||||
Regular → Executable
Reference in New Issue
Block a user