# 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 ```bash # 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 ... node 163_music_downloader.js --playlist= # 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 (`API` object 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). 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()`. - **Audio players** (`mpv`, `ffplay`, `play`, `cvlc`, `aplay`) — for the preview/play features, detected via `which`. Note: `which` is 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 around `https.get`. - **`downloadSong` → `_downloadSong`** — the core pipeline: `downloadSong` wraps `_downloadSong` with retry/backoff; `_downloadSong` resolves the URL, skips/overwrites based on existing-file size comparison (1% tolerance), writes audio, then lyrics, then embeds cover+tags, then records history. - **Lyrics** — `fetchLyric` returns `{ lrc, tlyric, ... }`; `mergeLrc` interleaves original + translation by matching `[mm:ss.xx]` timestamps into a `.合并翻译.lrc` file. - **Cover/tags** — `embedCover` downloads the image to a temp `.cover.jpg`, tries node-id3 (MP3) then ffmpeg (`-map` audio+cover, `-c copy`), always cleans up the temp file in `finally`. - **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 the `level` quality setting (`getConfig`/`setConfig`). - **Output layout** — audio + `.lrc` files go to `downloads/`. Deletes move files to `.trash/` (not permanent). ZIP packaging uses `.tmp_zip/`. The `c` menu option cleans these up. ## Conventions & gotchas - **Quality levels** — `QUALITY_LEVELS` array is the source of truth; `DEFAULT_LEVEL = 'jymaster'`. The chksz API may return a lower `actualLevel` than 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 `case` in the `main()` switch AND a line in both `printMenu()` and the `h` help text. Note the switch currently has a duplicated `case '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 the `p` hotkey (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.