neteasecli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/api/client.d.ts +19 -0
  4. package/dist/api/client.js +157 -0
  5. package/dist/api/crypto.d.ts +13 -0
  6. package/dist/api/crypto.js +59 -0
  7. package/dist/api/login.d.ts +8 -0
  8. package/dist/api/login.js +38 -0
  9. package/dist/api/playlist.d.ts +3 -0
  10. package/dist/api/playlist.js +65 -0
  11. package/dist/api/search.d.ts +2 -0
  12. package/dist/api/search.js +82 -0
  13. package/dist/api/track.d.ts +9 -0
  14. package/dist/api/track.js +96 -0
  15. package/dist/api/user.d.ts +5 -0
  16. package/dist/api/user.js +50 -0
  17. package/dist/auth/manager.d.ts +24 -0
  18. package/dist/auth/manager.js +108 -0
  19. package/dist/auth/storage.d.ts +7 -0
  20. package/dist/auth/storage.js +73 -0
  21. package/dist/cli/auth.d.ts +2 -0
  22. package/dist/cli/auth.js +62 -0
  23. package/dist/cli/index.d.ts +2 -0
  24. package/dist/cli/index.js +57 -0
  25. package/dist/cli/library.d.ts +2 -0
  26. package/dist/cli/library.js +92 -0
  27. package/dist/cli/player.d.ts +2 -0
  28. package/dist/cli/player.js +155 -0
  29. package/dist/cli/playlist.d.ts +2 -0
  30. package/dist/cli/playlist.js +60 -0
  31. package/dist/cli/search.d.ts +2 -0
  32. package/dist/cli/search.js +29 -0
  33. package/dist/cli/track.d.ts +2 -0
  34. package/dist/cli/track.js +119 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +18 -0
  37. package/dist/output/color.d.ts +7 -0
  38. package/dist/output/color.js +17 -0
  39. package/dist/output/json.d.ts +7 -0
  40. package/dist/output/json.js +201 -0
  41. package/dist/output/logger.d.ts +6 -0
  42. package/dist/output/logger.js +25 -0
  43. package/dist/player/mpv.d.ts +26 -0
  44. package/dist/player/mpv.js +160 -0
  45. package/dist/types/index.d.ts +68 -0
  46. package/dist/types/index.js +6 -0
  47. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wangwalk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # neteasecli
2
+
3
+ Netease Cloud Music CLI / 网易云音乐命令行工具
4
+
5
+ Search, play, download, and manage your library — all from the terminal with structured JSON output for scripting and AI agent integration.
6
+
7
+ ## Features
8
+
9
+ - Search tracks, albums, artists, playlists
10
+ - Playback via [mpv](https://mpv.io/) with IPC control (play/pause/stop/seek/volume/repeat)
11
+ - Track info, streaming URLs, lyrics, download
12
+ - Library management (liked tracks, recent history)
13
+ - Playlist browsing
14
+ - Browser cookie import from Chrome, Edge, Firefox, Safari via [sweet-cookie](https://github.com/nicolo-ribaudo/sweet-cookie)
15
+ - Multi-profile support for multiple accounts
16
+ - Three output modes: colorized human-readable, JSON, plain text
17
+ - Debug/verbose logging (`-v`, `-d`)
18
+ - Cross-platform: macOS, Linux, Windows
19
+
20
+ ## Why Cookies?
21
+
22
+ Netease Cloud Music has no public API. The unofficial API endpoints require encrypted requests and valid session cookies. Instead of implementing a fragile login flow (SMS/QR code), neteasecli imports cookies directly from your browser:
23
+
24
+ - **Multi-browser** — Chrome, Edge, Firefox, Safari (auto-detected)
25
+ - **No credentials stored** — reads browser's encrypted cookie DB via OS keychain
26
+ - **No captcha** — skip SMS verification entirely
27
+ - **Always fresh** — re-run `auth login` anytime to refresh
28
+ - **One command** — `neteasecli auth login` and you're in
29
+
30
+ ## Install / 安装
31
+
32
+ ```bash
33
+ # Run without installing / 免安装运行
34
+ npx neteasecli search track "Jay Chou"
35
+
36
+ # Install globally / 全局安装
37
+ npm install -g neteasecli
38
+ ```
39
+
40
+ ## Quick Start / 快速开始
41
+
42
+ ```bash
43
+ neteasecli auth login # Import cookies from Chrome
44
+ neteasecli search track "Sunny Day" # Search
45
+ neteasecli track play 185868 # Play (requires mpv)
46
+ neteasecli player pause # Pause/resume
47
+ neteasecli player stop # Stop
48
+ ```
49
+
50
+ ## Commands / 命令
51
+
52
+ ### auth
53
+
54
+ ```bash
55
+ neteasecli auth login # Import cookies from browser / 从浏览器导入 Cookie
56
+ neteasecli auth login --profile X # Specify Chrome/Edge profile / 指定 Chrome/Edge Profile
57
+ neteasecli auth check # Check login status / 检查登录状态
58
+ neteasecli auth logout # Logout / 登出
59
+ ```
60
+
61
+ ### search
62
+
63
+ ```bash
64
+ neteasecli search track <query> # Search tracks / 搜索歌曲
65
+ neteasecli search album <query> # Search albums / 搜索专辑
66
+ neteasecli search playlist <query> # Search playlists / 搜索歌单
67
+ neteasecli search artist <query> # Search artists / 搜索歌手
68
+ ```
69
+
70
+ Options: `-l, --limit <n>` (default 20), `-o, --offset <n>` (default 0)
71
+
72
+ ### track
73
+
74
+ ```bash
75
+ neteasecli track detail <id> # Track metadata / 歌曲详情
76
+ neteasecli track url <id> # Streaming URL / 播放链接
77
+ neteasecli track lyric <id> # Lyrics / 歌词
78
+ neteasecli track download <id> # Download / 下载
79
+ neteasecli track play <id> # Play via mpv / 用 mpv 播放
80
+ ```
81
+
82
+ Options: `-q, --quality <level>` standard | higher | exhigh (default) | lossless | hires
83
+
84
+ ### player
85
+
86
+ Requires [mpv](https://mpv.io/). / 需要安装 [mpv](https://mpv.io/)。
87
+
88
+ ```bash
89
+ neteasecli player status # Current playback status / 播放状态
90
+ neteasecli player pause # Toggle pause/resume / 暂停或继续
91
+ neteasecli player stop # Stop playback / 停止播放
92
+ neteasecli player seek <seconds> # Seek relative (e.g. 10, -10) / 快进快退
93
+ neteasecli player seek 30 --absolute # Seek to absolute position / 跳转到指定位置
94
+ neteasecli player volume [0-150] # Get or set volume / 音量
95
+ neteasecli player repeat [on|off] # Toggle or set repeat / 单曲循环
96
+ ```
97
+
98
+ ### library
99
+
100
+ ```bash
101
+ neteasecli library liked # Liked tracks / 喜欢的音乐
102
+ neteasecli library like <id> # Like a track / 收藏
103
+ neteasecli library unlike <id> # Unlike / 取消收藏
104
+ neteasecli library recent # Recently played / 最近播放
105
+ ```
106
+
107
+ ### playlist
108
+
109
+ ```bash
110
+ neteasecli playlist list # My playlists / 我的歌单
111
+ neteasecli playlist detail <id> # Playlist tracks / 歌单详情
112
+ ```
113
+
114
+ ## Global Options / 全局选项
115
+
116
+ | Flag | Description |
117
+ |------|-------------|
118
+ | `--json` | Force JSON output (default when piped) / 强制 JSON 输出 |
119
+ | `--plain` | Plain text output (tab-separated) / 纯文本输出 |
120
+ | `--pretty` | Pretty-print JSON / 格式化 JSON |
121
+ | `--quiet` | Suppress output / 静默模式 |
122
+ | `--no-color` | Disable colors / 禁用颜色 |
123
+ | `--profile <name>` | Account profile (default: "default") / 账号配置 |
124
+ | `-v, --verbose` | Verbose output / 详细输出 |
125
+ | `-d, --debug` | Debug output (implies --verbose) / 调试输出 |
126
+ | `--timeout <seconds>` | Request timeout (default: 30) / 请求超时秒数 |
127
+
128
+ ## Output Modes / 输出模式
129
+
130
+ | Mode | When | Description |
131
+ |------|------|-------------|
132
+ | Human | TTY (default) | Colorized, readable / 彩色可读 |
133
+ | JSON | Piped or `--json` | Structured `{ success, data, error }` / 结构化 JSON |
134
+ | Plain | `--plain` | Tab-separated, scriptable / 制表符分隔 |
135
+
136
+ ```bash
137
+ # Colorized output in terminal / 终端彩色输出
138
+ neteasecli search track "Jay Chou"
139
+
140
+ # JSON for scripting / JSON 用于脚本
141
+ neteasecli --json search track "Jay Chou"
142
+ neteasecli search track "Jay Chou" | jq '.data.tracks[0]'
143
+
144
+ # Plain text for cut/awk / 纯文本用于文本处理
145
+ neteasecli --plain search track "Jay Chou" | cut -f1,2
146
+ ```
147
+
148
+ Exit codes: `0` success, `1` general error, `2` auth error, `3` network error.
149
+
150
+ ## Multi-Profile / 多账号
151
+
152
+ ```bash
153
+ neteasecli --profile work auth login # Login with "work" profile
154
+ neteasecli --profile work library liked # Use "work" profile
155
+ neteasecli auth login # Default profile
156
+ ```
157
+
158
+ Profiles are stored in `~/.config/neteasecli/profiles/<name>/`.
159
+
160
+ ## Requirements / 环境要求
161
+
162
+ - Node.js >= 22
163
+ - Chrome, Edge, Firefox, or Safari (for cookie import / 用于导入 Cookie)
164
+ - [mpv](https://mpv.io/) (optional, for playback / 可选,用于播放)
165
+ - macOS, Linux, or Windows
166
+
167
+ ## Legal / 免责
168
+
169
+ This tool uses unofficial Netease Cloud Music API endpoints. Use responsibly and in accordance with Netease's Terms of Service.
170
+
171
+ 本工具使用非官方网易云音乐 API,请合理使用并遵守网易云音乐服务条款。
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,19 @@
1
+ export type CryptoType = 'weapi' | 'linuxapi' | 'eapi';
2
+ export interface RequestOptions {
3
+ crypto?: CryptoType;
4
+ url?: string;
5
+ }
6
+ export declare class ApiClient {
7
+ private client;
8
+ private readonly sDeviceId;
9
+ private readonly nmtid;
10
+ private sessionCookies;
11
+ constructor();
12
+ updateTimeout(ms: number): void;
13
+ private collectCookies;
14
+ private getCookieHeader;
15
+ request<T>(endpoint: string, data?: object, options?: RequestOptions): Promise<T>;
16
+ download(url: string, destPath: string): Promise<void>;
17
+ }
18
+ export declare function setRequestTimeout(ms: number): void;
19
+ export declare function getApiClient(): ApiClient;
@@ -0,0 +1,157 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import * as crypto from 'crypto';
3
+ import { weapi, linuxapi, eapi } from './crypto.js';
4
+ import { getAuthManager } from '../auth/manager.js';
5
+ import * as fs from 'fs';
6
+ import * as http from 'http';
7
+ import * as https from 'https';
8
+ import { pipeline } from 'stream/promises';
9
+ import { verbose, debug } from '../output/logger.js';
10
+ // Force IPv4 to avoid IPv6 CDN hotlink protection issues
11
+ const httpAgent = new http.Agent({ family: 4 });
12
+ const httpsAgent = new https.Agent({ family: 4 });
13
+ const BASE_URL = 'https://music.163.com';
14
+ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0';
15
+ export class ApiClient {
16
+ client;
17
+ sDeviceId = `unknown-${Math.floor(Math.random() * 1000000)}`;
18
+ nmtid = crypto.randomBytes(16).toString('hex');
19
+ sessionCookies = {};
20
+ constructor() {
21
+ this.client = axios.create({
22
+ baseURL: BASE_URL,
23
+ timeout: requestTimeout,
24
+ headers: {
25
+ 'User-Agent': USER_AGENT,
26
+ 'Content-Type': 'application/x-www-form-urlencoded',
27
+ Referer: 'https://music.163.com',
28
+ },
29
+ });
30
+ }
31
+ updateTimeout(ms) {
32
+ this.client.defaults.timeout = ms;
33
+ }
34
+ collectCookies(response) {
35
+ const setCookieHeaders = response.headers['set-cookie'];
36
+ if (!setCookieHeaders)
37
+ return;
38
+ for (const header of setCookieHeaders) {
39
+ const match = header.match(/^([^=]+)=([^;]*)/);
40
+ if (match) {
41
+ this.sessionCookies[match[1]] = match[2];
42
+ }
43
+ }
44
+ }
45
+ getCookieHeader(endpoint) {
46
+ const authManager = getAuthManager();
47
+ const userCookies = authManager.getCookieString();
48
+ const parts = ['os=pc', `sDeviceId=${this.sDeviceId}`, '__remember_me=true'];
49
+ if (endpoint && endpoint.includes('login')) {
50
+ parts.push(`NMTID=${this.nmtid}`);
51
+ }
52
+ if (userCookies) {
53
+ parts.push(userCookies);
54
+ }
55
+ for (const [name, value] of Object.entries(this.sessionCookies)) {
56
+ parts.push(`${name}=${value}`);
57
+ }
58
+ return parts.join('; ');
59
+ }
60
+ async request(endpoint, data = {}, options = {}) {
61
+ const { crypto: cryptoType = 'weapi' } = options;
62
+ let url;
63
+ let postData;
64
+ const requestData = { ...data };
65
+ switch (cryptoType) {
66
+ case 'weapi': {
67
+ url = `/weapi${endpoint}`;
68
+ const encrypted = weapi(requestData);
69
+ postData = {
70
+ params: encrypted.params,
71
+ encSecKey: encrypted.encSecKey,
72
+ };
73
+ break;
74
+ }
75
+ case 'linuxapi': {
76
+ url = '/api/linux/forward';
77
+ const encrypted = linuxapi({
78
+ method: 'POST',
79
+ url: `${BASE_URL}/api${endpoint}`,
80
+ params: requestData,
81
+ });
82
+ postData = { eparams: encrypted.eparams };
83
+ break;
84
+ }
85
+ case 'eapi': {
86
+ const eapiUrl = options.url || `/api${endpoint}`;
87
+ url = `/eapi${endpoint}`;
88
+ const encrypted = eapi(eapiUrl, requestData);
89
+ postData = { params: encrypted.params };
90
+ break;
91
+ }
92
+ }
93
+ verbose(`${cryptoType.toUpperCase()} ${endpoint}`);
94
+ debug(`POST ${url}`);
95
+ try {
96
+ const response = await this.client.post(url, new URLSearchParams(postData).toString(), {
97
+ headers: {
98
+ Cookie: this.getCookieHeader(endpoint),
99
+ },
100
+ });
101
+ this.collectCookies(response);
102
+ const responseData = response.data;
103
+ debug(`Response code: ${responseData.code ?? 200}`);
104
+ if (responseData.code && responseData.code !== 200) {
105
+ const msg = responseData.message || responseData.msg || 'Unknown error';
106
+ throw new Error(`${msg} (code: ${responseData.code})`);
107
+ }
108
+ return response.data;
109
+ }
110
+ catch (error) {
111
+ if (error instanceof AxiosError) {
112
+ if (error.response)
113
+ this.collectCookies(error.response);
114
+ debug(`HTTP error: ${error.response?.status ?? error.code}`);
115
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
116
+ throw new Error('Network connection failed');
117
+ }
118
+ if (error.response?.status === 401) {
119
+ throw new Error('Authentication failed, please re-login');
120
+ }
121
+ if (error.response?.status === 403) {
122
+ throw new Error('Access denied, login required or cookie expired');
123
+ }
124
+ throw new Error(`Request failed: ${error.message}`);
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ async download(url, destPath) {
130
+ verbose(`Downloading ${url}`);
131
+ const response = await axios.get(url, {
132
+ responseType: 'stream',
133
+ timeout: 120000,
134
+ httpAgent,
135
+ httpsAgent,
136
+ headers: {
137
+ 'User-Agent': USER_AGENT,
138
+ Referer: 'https://music.163.com/',
139
+ },
140
+ });
141
+ await pipeline(response.data, fs.createWriteStream(destPath));
142
+ }
143
+ }
144
+ let requestTimeout = 30000;
145
+ export function setRequestTimeout(ms) {
146
+ requestTimeout = ms;
147
+ if (clientInstance) {
148
+ clientInstance.updateTimeout(ms);
149
+ }
150
+ }
151
+ let clientInstance = null;
152
+ export function getApiClient() {
153
+ if (!clientInstance) {
154
+ clientInstance = new ApiClient();
155
+ }
156
+ return clientInstance;
157
+ }
@@ -0,0 +1,13 @@
1
+ export interface WeapiResult {
2
+ params: string;
3
+ encSecKey: string;
4
+ }
5
+ export declare function weapi(data: object): WeapiResult;
6
+ export interface LinuxapiResult {
7
+ eparams: string;
8
+ }
9
+ export declare function linuxapi(data: object): LinuxapiResult;
10
+ export interface EapiResult {
11
+ params: string;
12
+ }
13
+ export declare function eapi(url: string, data: object): EapiResult;
@@ -0,0 +1,59 @@
1
+ import * as crypto from 'crypto';
2
+ const IV = '0102030405060708';
3
+ const PRESET_KEY = '0CoJUm6Qyw8W8jud';
4
+ const PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----';
5
+ const EAPI_KEY = 'e82ckenh8dichen8';
6
+ const LINUX_API_KEY = 'rFgB&h#%2?^eDg:Q';
7
+ function createSecretKey(size) {
8
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
9
+ let key = '';
10
+ for (let i = 0; i < size; i++) {
11
+ key += chars.charAt(Math.floor(Math.random() * chars.length));
12
+ }
13
+ return key;
14
+ }
15
+ function aesEncrypt(text, key, iv = IV) {
16
+ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
17
+ let encrypted = cipher.update(text, 'utf8', 'base64');
18
+ encrypted += cipher.final('base64');
19
+ return encrypted;
20
+ }
21
+ function rsaEncrypt(text, pubKey) {
22
+ const reversedText = text.split('').reverse().join('');
23
+ const buffer = Buffer.alloc(128, 0);
24
+ const textBuffer = Buffer.from(reversedText);
25
+ textBuffer.copy(buffer, 128 - textBuffer.length);
26
+ const encrypted = crypto.publicEncrypt({
27
+ key: pubKey,
28
+ padding: crypto.constants.RSA_NO_PADDING,
29
+ }, buffer);
30
+ return encrypted.toString('hex');
31
+ }
32
+ function md5(text) {
33
+ return crypto.createHash('md5').update(text).digest('hex');
34
+ }
35
+ export function weapi(data) {
36
+ const text = JSON.stringify(data);
37
+ const secretKey = createSecretKey(16);
38
+ const params1 = aesEncrypt(text, PRESET_KEY, IV);
39
+ const params = aesEncrypt(params1, secretKey, IV);
40
+ const encSecKey = rsaEncrypt(secretKey, PUBLIC_KEY);
41
+ return { params, encSecKey };
42
+ }
43
+ export function linuxapi(data) {
44
+ const text = JSON.stringify(data);
45
+ const cipher = crypto.createCipheriv('aes-128-ecb', LINUX_API_KEY, '');
46
+ let eparams = cipher.update(text, 'utf8', 'hex').toUpperCase();
47
+ eparams += cipher.final('hex').toUpperCase();
48
+ return { eparams };
49
+ }
50
+ export function eapi(url, data) {
51
+ const text = JSON.stringify(data);
52
+ const message = `nobody${url}use${text}md5forencrypt`;
53
+ const digest = md5(message);
54
+ const params = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
55
+ const cipher = crypto.createCipheriv('aes-128-ecb', EAPI_KEY, '');
56
+ let encrypted = cipher.update(params, 'utf8', 'hex').toUpperCase();
57
+ encrypted += cipher.final('hex').toUpperCase();
58
+ return { params: encrypted };
59
+ }
@@ -0,0 +1,8 @@
1
+ import type { AxiosResponse } from 'axios';
2
+ import type { CookieData } from '../types/index.js';
3
+ export declare function extractCookiesFromResponse(response: AxiosResponse): CookieData;
4
+ export declare function sendCaptcha(phone: string, countrycode?: string): Promise<void>;
5
+ export declare function loginByCaptcha(phone: string, captcha: string, countrycode?: string): Promise<{
6
+ cookies: CookieData;
7
+ profile: any;
8
+ }>;
@@ -0,0 +1,38 @@
1
+ import { getApiClient } from './client.js';
2
+ export function extractCookiesFromResponse(response) {
3
+ const cookies = {};
4
+ const setCookieHeaders = response.headers['set-cookie'];
5
+ if (!setCookieHeaders)
6
+ return cookies;
7
+ for (const header of setCookieHeaders) {
8
+ const match = header.match(/^([^=]+)=([^;]*)/);
9
+ if (match) {
10
+ const [, name, value] = match;
11
+ if (name === 'MUSIC_U' || name === '__csrf') {
12
+ cookies[name] = value;
13
+ }
14
+ }
15
+ }
16
+ return cookies;
17
+ }
18
+ export async function sendCaptcha(phone, countrycode = '86') {
19
+ const client = getApiClient();
20
+ await client.request('/sms/captcha/sent', {
21
+ cellphone: phone,
22
+ ctcode: countrycode,
23
+ }, { crypto: 'weapi' });
24
+ }
25
+ export async function loginByCaptcha(phone, captcha, countrycode = '86') {
26
+ const client = getApiClient();
27
+ const response = await client.requestRaw('/login/cellphone', {
28
+ phone,
29
+ captcha,
30
+ countrycode,
31
+ rememberLogin: 'true',
32
+ }, { crypto: 'weapi' });
33
+ const cookies = extractCookiesFromResponse(response);
34
+ if (!cookies.MUSIC_U) {
35
+ throw new Error('登录失败:未获取到 MUSIC_U cookie');
36
+ }
37
+ return { cookies, profile: response.data.profile };
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { Playlist } from '../types/index.js';
2
+ export declare function getPlaylistDetail(id: string): Promise<Playlist>;
3
+ export declare function getUserPlaylists(uid?: string): Promise<Playlist[]>;
@@ -0,0 +1,65 @@
1
+ import { getApiClient } from './client.js';
2
+ function transformTrack(track) {
3
+ const artists = track.ar || track.artists || [];
4
+ const album = track.al || track.album || { id: 0, name: '' };
5
+ return {
6
+ id: String(track.id),
7
+ name: track.name,
8
+ artists: artists.map((a) => ({ id: String(a.id), name: a.name })),
9
+ album: {
10
+ id: String(album.id),
11
+ name: album.name,
12
+ picUrl: album.picUrl,
13
+ },
14
+ duration: track.dt || track.duration || 0,
15
+ uri: `netease:track:${track.id}`,
16
+ };
17
+ }
18
+ export async function getPlaylistDetail(id) {
19
+ const client = getApiClient();
20
+ const response = await client.request('/v6/playlist/detail', {
21
+ id,
22
+ n: 100000,
23
+ });
24
+ const playlist = response.playlist;
25
+ return {
26
+ id: String(playlist.id),
27
+ name: playlist.name,
28
+ description: playlist.description,
29
+ coverUrl: playlist.coverImgUrl,
30
+ trackCount: playlist.trackCount,
31
+ creator: playlist.creator
32
+ ? {
33
+ id: String(playlist.creator.userId),
34
+ name: playlist.creator.nickname,
35
+ }
36
+ : undefined,
37
+ tracks: playlist.tracks?.map(transformTrack),
38
+ };
39
+ }
40
+ export async function getUserPlaylists(uid) {
41
+ const client = getApiClient();
42
+ let userId = uid;
43
+ if (!userId) {
44
+ const userInfo = await client.request('/nuser/account/get');
45
+ userId = String(userInfo.profile.userId);
46
+ }
47
+ const response = await client.request('/user/playlist', {
48
+ uid: userId,
49
+ limit: 1000,
50
+ offset: 0,
51
+ });
52
+ return response.playlist.map((p) => ({
53
+ id: String(p.id),
54
+ name: p.name,
55
+ description: p.description,
56
+ coverUrl: p.coverImgUrl,
57
+ trackCount: p.trackCount,
58
+ creator: p.creator
59
+ ? {
60
+ id: String(p.creator.userId),
61
+ name: p.creator.nickname,
62
+ }
63
+ : undefined,
64
+ }));
65
+ }
@@ -0,0 +1,2 @@
1
+ import type { SearchType, SearchResult } from '../types/index.js';
2
+ export declare function search(keyword: string, type?: SearchType, limit?: number, offset?: number): Promise<SearchResult>;
@@ -0,0 +1,82 @@
1
+ import { getApiClient } from './client.js';
2
+ const typeMap = {
3
+ track: 1,
4
+ album: 10,
5
+ playlist: 1000,
6
+ artist: 100,
7
+ };
8
+ function transformTrack(track) {
9
+ return {
10
+ id: String(track.id),
11
+ name: track.name,
12
+ artists: track.ar.map((a) => ({ id: String(a.id), name: a.name })),
13
+ album: {
14
+ id: String(track.al.id),
15
+ name: track.al.name,
16
+ picUrl: track.al.picUrl,
17
+ },
18
+ duration: track.dt,
19
+ uri: `netease:track:${track.id}`,
20
+ };
21
+ }
22
+ function transformAlbum(album) {
23
+ return {
24
+ id: String(album.id),
25
+ name: album.name,
26
+ picUrl: album.picUrl,
27
+ };
28
+ }
29
+ function transformPlaylist(playlist) {
30
+ return {
31
+ id: String(playlist.id),
32
+ name: playlist.name,
33
+ description: playlist.description,
34
+ coverUrl: playlist.coverImgUrl,
35
+ trackCount: playlist.trackCount,
36
+ creator: playlist.creator
37
+ ? {
38
+ id: String(playlist.creator.userId),
39
+ name: playlist.creator.nickname,
40
+ }
41
+ : undefined,
42
+ };
43
+ }
44
+ function transformArtist(artist) {
45
+ return {
46
+ id: String(artist.id),
47
+ name: artist.name,
48
+ };
49
+ }
50
+ export async function search(keyword, type = 'track', limit = 20, offset = 0) {
51
+ const client = getApiClient();
52
+ const response = await client.request('/cloudsearch/get/web', {
53
+ s: keyword,
54
+ type: typeMap[type],
55
+ limit,
56
+ offset,
57
+ });
58
+ const result = {
59
+ total: 0,
60
+ offset,
61
+ limit,
62
+ };
63
+ switch (type) {
64
+ case 'track':
65
+ result.tracks = (response.result.songs || []).map(transformTrack);
66
+ result.total = response.result.songCount || 0;
67
+ break;
68
+ case 'album':
69
+ result.albums = (response.result.albums || []).map(transformAlbum);
70
+ result.total = response.result.albumCount || 0;
71
+ break;
72
+ case 'playlist':
73
+ result.playlists = (response.result.playlists || []).map(transformPlaylist);
74
+ result.total = response.result.playlistCount || 0;
75
+ break;
76
+ case 'artist':
77
+ result.artists = (response.result.artists || []).map(transformArtist);
78
+ result.total = response.result.artistCount || 0;
79
+ break;
80
+ }
81
+ return result;
82
+ }
@@ -0,0 +1,9 @@
1
+ import type { Track, Lyric, Quality } from '../types/index.js';
2
+ export declare function getTrackDetail(id: string): Promise<Track>;
3
+ export declare function getTrackUrl(id: string, quality?: Quality): Promise<string>;
4
+ export declare function getLyric(id: string): Promise<Lyric>;
5
+ export declare function downloadTrack(id: string, quality?: Quality, outputPath?: string): Promise<{
6
+ path: string;
7
+ size: number;
8
+ }>;
9
+ export declare function getTrackDetails(ids: string[]): Promise<Track[]>;