4.5 KiB
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Overview
A single-file Node.js CLI for downloading music from NetEase Cloud Music (网易云音乐), with lyrics, cover art, and ID3 tag embedding. All logic lives in 163_music_downloader.js (~2600 lines). There is no package.json — the script runs on Node built-ins only (https, http, fs, path, readline, child_process). The UI, comments, and console output are entirely in Chinese.
Running
# Interactive menu (TTY required for pause hotkey)
node 163_music_downloader.js
# Command-line batch mode (bypasses the menu entirely)
node 163_music_downloader.js <songId> <songId> ...
node 163_music_downloader.js --playlist=<playlistId>
# Flags (batch mode only)
--level=lossless # override quality; see QUALITY_LEVELS
--retries=5 # download retry count (default 3)
--no-lyric # skip .lrc download
--no-cover # skip cover/tag embedding
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 (APIobject at top of file) for song URLs, search, lyrics, and playlists. This is the core download source.music.163.com— queried directly (fetchNeteaseSongDetail,fetchNeteaseAlbumDetail) for rich metadata (album, publish date, popularity). RequiresCookie: os=pcheader.node-id3(npm) — preferred for MP3 tag/lyric/cover embedding.required lazily viahasNodeID3(); absent by default.ffmpeg(system) — fallback embedder for non-MP3 (FLAC etc.) and when node-id3 is missing. Detected viahasFFmpeg().- Audio players (
mpv,ffplay,play,cvlc,aplay) — for the preview/play features, detected viawhich. Note:whichis Unix-only, so preview won't find players on Windows even if installed.
If a dependency is missing the relevant feature degrades gracefully (skips embedding, prints the URL, etc.).
Architecture
Everything is one flat module — functions defined top-to-bottom, then a main() at the bottom drives either batch mode or an infinite interactive menu loop (while (true) + switch on single-char menu choices). Key layers:
- Network helpers —
fetchJSON,downloadFile(streams with a live progress bar, follows redirects),downloadBuffer, all Promise-wrapped aroundhttps.get. downloadSong→_downloadSong— the core pipeline:downloadSongwraps_downloadSongwith retry/backoff;_downloadSongresolves the URL, skips/overwrites based on existing-file size comparison (1% tolerance), writes audio, then lyrics, then embeds cover+tags, then records history.- Lyrics —
fetchLyricreturns{ lrc, tlyric, ... };mergeLrcinterleaves original + translation by matching[mm:ss.xx]timestamps into a.合并翻译.lrcfile. - Cover/tags —
embedCoverdownloads the image to a temp.cover.jpg, tries node-id3 (MP3) then ffmpeg (-mapaudio+cover,-c copy), always cleans up the temp file infinally. - Persistence (all JSON in the script's
__dirname, not the download dir):download_history.json— capped at 500 entries, newest-first (addHistory).downloader_config.json— key/value config, currently only thelevelquality setting (getConfig/setConfig).
- Output layout — audio +
.lrcfiles go todownloads/. Deletes move files to.trash/(not permanent). ZIP packaging uses.tmp_zip/. Thecmenu option cleans these up.
Conventions & gotchas
- Quality levels —
QUALITY_LEVELSarray is the source of truth;DEFAULT_LEVEL = 'jymaster'. The chksz API may return a loweractualLevelthan requested. - Filenames — always
sanitize(${artist} - ${name})(strips/\:*?"<>|). Lyric/rename logic depends on this exact"artist - name"shape when parsing filenames back out. - Menu dispatch — adding a feature means adding a
casein themain()switch AND a line in bothprintMenu()and thehhelp text. Note the switch currently has a duplicatedcase 'p'/'P'block (the second is unreachable) and the keypress listener is registered twice — mirror the existing style rather than assuming it's clean. - Pause — global
isPaused/waitIfPaused()gate batch loops; toggled by thephotkey (only wired up in interactive mode with a TTY). - Batch loops sleep ~300–500ms between songs to avoid hammering the API — preserve this when editing download loops.