From 5bd62645ab38acd3a35ab0cc417d11b928c5fec2 Mon Sep 17 00:00:00 2001 From: Jocay <1579649885@qq.com> Date: Sat, 10 Oct 2026 20:22:49 +0800 Subject: [PATCH] feat: migrate ChKSz API and support API key authentication --- 163_music_downloader.js | 37 +++++++++++++++++----------------- AGENTS.md | 2 +- CLAUDE.md | 2 +- README.md | 11 +++++----- SKILL.md | 10 +++++---- config.json | 9 +++++---- lib/catalog.js | 10 ++++----- lib/config.js | 24 ++++++++++++++++++---- lib/downloader.js | 4 ++-- lib/lyrics.js | 4 ++-- lib/metadata.js | 4 ++-- lib/network.js | 14 ++++++++++++- normalize_person_separators.js | 0 13 files changed, 82 insertions(+), 49 deletions(-) mode change 100644 => 100755 normalize_person_separators.js diff --git a/163_music_downloader.js b/163_music_downloader.js index 01f778a..39fc613 100755 --- a/163_music_downloader.js +++ b/163_music_downloader.js @@ -14,6 +14,7 @@ const { DEFAULT_RETRIES, getConfig, setConfig, + buildApiUrl, } = require('./lib/config'); const { fetchJSON, downloadFile } = require('./lib/network'); const { sanitize, sleep, getDirSize } = require('./lib/utils'); @@ -479,7 +480,7 @@ async function main() { console.log(`\n🔍 获取试听链接 (ID: ${songId}) ...`); 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) { console.log(`❌ 获取失败: ${result.msg || '未知错误'}`); break; @@ -729,7 +730,7 @@ async function main() { const s = results[idx - 1]; console.log(`\n ▶️ 试听: ${s.name} - ${s.artists}`); 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) { const players = [ { cmd: 'mpv', args: (u) => ['--no-video', u] }, @@ -763,7 +764,7 @@ async function main() { if (idx >= 1 && idx <= results.length) { const s = results[idx - 1]; 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) { const d = r.data; console.log('\n ┌─────────────────────────────────────────┐'); @@ -1234,9 +1235,9 @@ async function main() { 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]; + const songs = await searchSongs(keyword, 3); + if (songs.length) { + const best = songs[0]; resolvedIds.push(best.id); console.log(` ✅ "${nameLine}" → ${best.name} - ${best.artists} (ID: ${best.id})`); } else { @@ -1357,24 +1358,24 @@ async function main() { 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) { + const results = await searchSongs(keyword, 5); + if (!results.length) { console.log(' ⚠️ 未找到匹配'); continue; } // 找最佳匹配 - const best = results.data.find(s => { + const best = results.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]; + }) || results[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`); + const songInfo = await fetchJSON(buildApiUrl(API.music, { id: best.id, level: 'standard' })); if (songInfo.code === 200 && songInfo.data) { const d = songInfo.data; if (d.picUrl) { @@ -1523,7 +1524,7 @@ async function main() { if (!found) { // 用 API 链接作为备选 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(''); } @@ -1615,24 +1616,24 @@ async function main() { try { // 搜索匹配 const keyword = `${artist} ${name}`; - const results = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=5`); - if (results.code !== 200 || !results.data?.length) { + const results = await searchSongs(keyword, 5); + if (!results.length) { console.log(' ⚠️ 搜索无结果,跳过'); fail++; continue; } // 找最佳匹配 - const best = results.data.find(s => { + const best = results.find(s => { const sName = (s.name || '').toLowerCase(); const sArtist = (s.artists || '').toLowerCase(); return sName.includes(name.toLowerCase()) && sArtist.includes(artist.toLowerCase()); - }) || results.data[0]; + }) || results[0]; 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) { const d = songInfo.data; const filePath = path.join(DOWNLOAD_DIR, f); @@ -1698,7 +1699,7 @@ async function main() { console.log(`[${i + 1}/${pl.tracks.length}] ${title} - ${artist}`); 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) { console.log(' ❌ 解析失败'); failed++; diff --git a/AGENTS.md b/AGENTS.md index 47758f2..18da2a2 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ There are no tests, linter, or build step. Verify changes by running the script. ## 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. - **`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()`. diff --git a/CLAUDE.md b/CLAUDE.md index 4e0659a..bbe0803 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ There are no tests, linter, or build step. Verify changes by running the script. ## 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. - **`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()`. diff --git a/README.md b/README.md index e0bcc88..1eb890c 100755 --- a/README.md +++ b/README.md @@ -115,10 +115,11 @@ node 163_music_downloader.js 347230 --level=hires --no-cover ```json { "api": { - "music": "https://api.chksz.top/api/163_music", - "lyric": "https://api.chksz.top/api/163_lyric", - "search": "https://api.chksz.top/api/163_search", - "playlist": "https://api.chksz.top/api/163_playlist", + "apikey": "YOUR_API_KEY", + "music": "https://api.chksz.com/api/163_music", + "lyric": "https://api.chksz.com/api/163_lyric", + "search": "https://api.chksz.com/api/163_search", + "playlist": "https://api.chksz.com/api/163_playlist", "neteaseBaseUrl": "https://music.163.com" }, "paths": { @@ -151,7 +152,7 @@ node 163_music_downloader.js 347230 --level=hires --no-cover ## 注意事项 -- 下载能力依赖第三方 `api.chksz.top` 接口,其可用性、音源和返回质量不由本项目保证。 +- 下载能力依赖第三方 `api.chksz.com` 接口(需配置 `apikey`),其可用性、音源和返回质量不由本项目保证。 - 歌曲详情和专辑元数据会直接请求网易云音乐。 - 请遵守当地法律法规、平台条款和音乐版权要求,仅将本工具用于合法用途。 - 音频文件和压缩包已通过 `.gitignore` 排除,不会被提交到仓库。 diff --git a/SKILL.md b/SKILL.md index 18ae8be..8fd9d4b 100755 --- a/SKILL.md +++ b/SKILL.md @@ -49,10 +49,11 @@ node 163_music_downloader.js --album=专辑ID ```json { "api": { - "music": "https://api.chksz.top/api/163_music", - "lyric": "https://api.chksz.top/api/163_lyric", - "search": "https://api.chksz.top/api/163_search", - "playlist": "https://api.chksz.top/api/163_playlist", + "apikey": "YOUR_API_KEY", + "music": "https://api.chksz.com/api/163_music", + "lyric": "https://api.chksz.com/api/163_lyric", + "search": "https://api.chksz.com/api/163_search", + "playlist": "https://api.chksz.com/api/163_playlist", "neteaseBaseUrl": "https://music.163.com" }, "paths": { @@ -72,6 +73,7 @@ node 163_music_downloader.js --album=专辑ID 按以下规则设置: +- 使用 `api.apikey` 设置 ChKSz API 个人鉴权密钥。 - 使用 `api.music` 获取音频地址和基础歌曲信息。 - 使用 `api.lyric` 获取原文和翻译歌词。 - 使用 `api.search` 搜索歌曲、歌手和专辑。 diff --git a/config.json b/config.json index bac20f2..e4bda93 100755 --- a/config.json +++ b/config.json @@ -1,9 +1,10 @@ { "api": { - "music": "https://api.chksz.top/api/163_music", - "lyric": "https://api.chksz.top/api/163_lyric", - "search": "https://api.chksz.top/api/163_search", - "playlist": "https://api.chksz.top/api/163_playlist", + "apikey": "", + "music": "https://api.chksz.com/api/163_music", + "lyric": "https://api.chksz.com/api/163_lyric", + "search": "https://api.chksz.com/api/163_search", + "playlist": "https://api.chksz.com/api/163_playlist", "neteaseBaseUrl": "https://music.163.com" }, "paths": { diff --git a/lib/catalog.js b/lib/catalog.js index ad881fc..2f18c4d 100755 --- a/lib/catalog.js +++ b/lib/catalog.js @@ -1,11 +1,11 @@ -const { API } = require('./config'); +const { API, buildApiUrl } = require('./config'); const { fetchJSON } = require('./network'); const { formatPersonNames } = require('./utils'); 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) { - console.log('❌ 搜索无结果'); + console.log(`❌ 搜索失败: ${res.msg || '无结果'}`); return []; } const songs = Array.isArray(res.data) ? res.data : (res.data?.songs || []); @@ -40,9 +40,9 @@ async function interactiveSearch(rl, ask) { } 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) { - console.log('❌ 歌单获取失败或为空'); + console.log(`❌ 歌单获取失败或为空${res.msg ? `: ${res.msg}` : ''}`); return null; } return res.data; diff --git a/lib/config.js b/lib/config.js index c371539..370751f 100755 --- a/lib/config.js +++ b/lib/config.js @@ -5,10 +5,11 @@ const ROOT_DIR = path.join(__dirname, '..'); const APP_CONFIG_FILE = path.join(ROOT_DIR, 'config.json'); const DEFAULT_APP_CONFIG = { api: { - music: 'https://api.chksz.top/api/163_music', - lyric: 'https://api.chksz.top/api/163_lyric', - search: 'https://api.chksz.top/api/163_search', - playlist: 'https://api.chksz.top/api/163_playlist', + apikey: process.env.CHKSZ_API_KEY || '', + music: 'https://api.chksz.com/api/163_music', + lyric: 'https://api.chksz.com/api/163_lyric', + search: 'https://api.chksz.com/api/163_search', + playlist: 'https://api.chksz.com/api/163_playlist', neteaseBaseUrl: 'https://music.163.com', }, paths: { @@ -51,12 +52,26 @@ function getConfigString(value, fallback) { const APP_CONFIG = loadAppConfig(); const API = { + apikey: getConfigString(APP_CONFIG.api.apikey, DEFAULT_APP_CONFIG.api.apikey), music: getConfigString(APP_CONFIG.api.music, DEFAULT_APP_CONFIG.api.music), lyric: getConfigString(APP_CONFIG.api.lyric, DEFAULT_APP_CONFIG.api.lyric), search: getConfigString(APP_CONFIG.api.search, DEFAULT_APP_CONFIG.api.search), playlist: getConfigString(APP_CONFIG.api.playlist, DEFAULT_APP_CONFIG.api.playlist), 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 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); @@ -123,4 +138,5 @@ module.exports = { HISTORY_LIMIT, getConfig, setConfig, + buildApiUrl, }; diff --git a/lib/downloader.js b/lib/downloader.js index 7c0ec22..77b94d4 100755 --- a/lib/downloader.js +++ b/lib/downloader.js @@ -1,6 +1,6 @@ const fs = require('fs'); 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 { fetchLyric, mergeLrc, parseCredits } = require('./lyrics'); const { fetchNeteaseAlbumDetail, fetchNeteaseSongDetail } = require('./metadata'); @@ -39,7 +39,7 @@ async function downloadSongOnce(songId, level, { outputBaseName = null, } = {}) { 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 返回错误'); const { name, artist: rawArtist, album, url: audioUrl, br, size, level: actualLevel, picUrl } = result.data; diff --git a/lib/lyrics.js b/lib/lyrics.js index 4fcf6c2..43f0792 100755 --- a/lib/lyrics.js +++ b/lib/lyrics.js @@ -1,12 +1,12 @@ const fs = require('fs'); const path = require('path'); -const { API, DOWNLOAD_DIR } = require('./config'); +const { API, DOWNLOAD_DIR, buildApiUrl } = require('./config'); const { fetchJSON } = require('./network'); const { formatPersonNames } = require('./utils'); async function fetchLyric(songId) { try { - const res = await fetchJSON(`${API.lyric}?id=${songId}`); + const res = await fetchJSON(buildApiUrl(API.lyric, { id: songId })); if (res.code !== 200) return null; return res.data; } catch { diff --git a/lib/metadata.js b/lib/metadata.js index c796778..9d51118 100755 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -2,7 +2,7 @@ const fs = require('fs'); const http = require('http'); const https = require('https'); 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 { sanitizePathSegment } = require('./utils'); @@ -127,7 +127,7 @@ async function saveAlbumCover(coverUrl, outputDir) { async function fetchFullMetadata(songId) { const [songDetail, chkszData] = await Promise.all([ 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; diff --git a/lib/network.js b/lib/network.js index f562a6b..be74592 100755 --- a/lib/network.js +++ b/lib/network.js @@ -7,8 +7,20 @@ function getProtocol(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) => { - 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 = ''; res.on('data', chunk => data += chunk); res.on('end', () => { diff --git a/normalize_person_separators.js b/normalize_person_separators.js old mode 100644 new mode 100755