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.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/api/client.d.ts +19 -0
- package/dist/api/client.js +157 -0
- package/dist/api/crypto.d.ts +13 -0
- package/dist/api/crypto.js +59 -0
- package/dist/api/login.d.ts +8 -0
- package/dist/api/login.js +38 -0
- package/dist/api/playlist.d.ts +3 -0
- package/dist/api/playlist.js +65 -0
- package/dist/api/search.d.ts +2 -0
- package/dist/api/search.js +82 -0
- package/dist/api/track.d.ts +9 -0
- package/dist/api/track.js +96 -0
- package/dist/api/user.d.ts +5 -0
- package/dist/api/user.js +50 -0
- package/dist/auth/manager.d.ts +24 -0
- package/dist/auth/manager.js +108 -0
- package/dist/auth/storage.d.ts +7 -0
- package/dist/auth/storage.js +73 -0
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +62 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +57 -0
- package/dist/cli/library.d.ts +2 -0
- package/dist/cli/library.js +92 -0
- package/dist/cli/player.d.ts +2 -0
- package/dist/cli/player.js +155 -0
- package/dist/cli/playlist.d.ts +2 -0
- package/dist/cli/playlist.js +60 -0
- package/dist/cli/search.d.ts +2 -0
- package/dist/cli/search.js +29 -0
- package/dist/cli/track.d.ts +2 -0
- package/dist/cli/track.js +119 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/output/color.d.ts +7 -0
- package/dist/output/color.js +17 -0
- package/dist/output/json.d.ts +7 -0
- package/dist/output/json.js +201 -0
- package/dist/output/logger.d.ts +6 -0
- package/dist/output/logger.js +25 -0
- package/dist/player/mpv.d.ts +26 -0
- package/dist/player/mpv.js +160 -0
- package/dist/types/index.d.ts +68 -0
- package/dist/types/index.js +6 -0
- package/package.json +63 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare class MpvPlayer {
|
|
2
|
+
private requestId;
|
|
3
|
+
play(url: string, title?: string): Promise<void>;
|
|
4
|
+
pause(): Promise<void>;
|
|
5
|
+
seek(seconds: number, mode?: 'relative' | 'absolute'): Promise<void>;
|
|
6
|
+
setVolume(volume: number): Promise<void>;
|
|
7
|
+
getVolume(): Promise<number>;
|
|
8
|
+
setLoop(mode: 'no' | 'inf' | 'force'): Promise<void>;
|
|
9
|
+
getLoop(): Promise<string>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
getStatus(): Promise<{
|
|
12
|
+
title?: string;
|
|
13
|
+
position: number;
|
|
14
|
+
duration: number;
|
|
15
|
+
paused: boolean;
|
|
16
|
+
playing: boolean;
|
|
17
|
+
volume: number;
|
|
18
|
+
loop: string;
|
|
19
|
+
}>;
|
|
20
|
+
isRunning(): Promise<boolean>;
|
|
21
|
+
private getProperty;
|
|
22
|
+
private setProperty;
|
|
23
|
+
private sendCommand;
|
|
24
|
+
}
|
|
25
|
+
export declare const mpvPlayer: MpvPlayer;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as net from 'net';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
const SOCKET_PATH = process.platform === 'win32' ? '\\\\.\\pipe\\neteasecli-mpv' : '/tmp/neteasecli-mpv.sock';
|
|
5
|
+
class MpvPlayer {
|
|
6
|
+
requestId = 0;
|
|
7
|
+
async play(url, title) {
|
|
8
|
+
// Stop any existing instance
|
|
9
|
+
await this.stop().catch(() => { });
|
|
10
|
+
// Clean up stale socket file
|
|
11
|
+
if (process.platform !== 'win32' && fs.existsSync(SOCKET_PATH)) {
|
|
12
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const proc = spawn('mpv', [
|
|
16
|
+
'--no-video',
|
|
17
|
+
`--input-ipc-server=${SOCKET_PATH}`,
|
|
18
|
+
`--title=${title || 'neteasecli'}`,
|
|
19
|
+
url,
|
|
20
|
+
], {
|
|
21
|
+
stdio: 'ignore',
|
|
22
|
+
detached: true,
|
|
23
|
+
});
|
|
24
|
+
proc.unref();
|
|
25
|
+
proc.on('error', (err) => {
|
|
26
|
+
reject(new Error(`Failed to start mpv: ${err.message}`));
|
|
27
|
+
});
|
|
28
|
+
// Wait for socket to become available
|
|
29
|
+
let attempts = 0;
|
|
30
|
+
const waitForSocket = () => {
|
|
31
|
+
attempts++;
|
|
32
|
+
const sock = net.createConnection(SOCKET_PATH);
|
|
33
|
+
sock.on('connect', () => {
|
|
34
|
+
sock.destroy();
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
sock.on('error', () => {
|
|
38
|
+
sock.destroy();
|
|
39
|
+
if (attempts < 10) {
|
|
40
|
+
setTimeout(waitForSocket, 200);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
reject(new Error('mpv started but IPC socket not available'));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
setTimeout(waitForSocket, 300);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async pause() {
|
|
51
|
+
await this.sendCommand(['cycle', 'pause']);
|
|
52
|
+
}
|
|
53
|
+
async seek(seconds, mode = 'relative') {
|
|
54
|
+
await this.sendCommand(['seek', String(seconds), mode]);
|
|
55
|
+
}
|
|
56
|
+
async setVolume(volume) {
|
|
57
|
+
await this.setProperty('volume', Math.max(0, Math.min(150, volume)));
|
|
58
|
+
}
|
|
59
|
+
async getVolume() {
|
|
60
|
+
const vol = await this.getProperty('volume').catch(() => 100);
|
|
61
|
+
return Number(vol) || 100;
|
|
62
|
+
}
|
|
63
|
+
async setLoop(mode) {
|
|
64
|
+
await this.setProperty('loop-file', mode);
|
|
65
|
+
}
|
|
66
|
+
async getLoop() {
|
|
67
|
+
const loop = await this.getProperty('loop-file').catch(() => 'no');
|
|
68
|
+
return String(loop);
|
|
69
|
+
}
|
|
70
|
+
async stop() {
|
|
71
|
+
try {
|
|
72
|
+
await this.sendCommand(['quit']);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// mpv not running, ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async getStatus() {
|
|
79
|
+
try {
|
|
80
|
+
const [position, duration, paused, volume, loop, title] = await Promise.all([
|
|
81
|
+
this.getProperty('time-pos').catch(() => 0),
|
|
82
|
+
this.getProperty('duration').catch(() => 0),
|
|
83
|
+
this.getProperty('pause').catch(() => false),
|
|
84
|
+
this.getProperty('volume').catch(() => 100),
|
|
85
|
+
this.getProperty('loop-file').catch(() => 'no'),
|
|
86
|
+
this.getProperty('media-title').catch(() => undefined),
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
title: title ? String(title) : undefined,
|
|
90
|
+
position: Number(position) || 0,
|
|
91
|
+
duration: Number(duration) || 0,
|
|
92
|
+
paused: Boolean(paused),
|
|
93
|
+
playing: true,
|
|
94
|
+
volume: Number(volume) || 100,
|
|
95
|
+
loop: String(loop),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { position: 0, duration: 0, paused: false, playing: false, volume: 100, loop: 'no' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async isRunning() {
|
|
103
|
+
try {
|
|
104
|
+
await this.getProperty('pid');
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getProperty(name) {
|
|
112
|
+
return this.sendCommand(['get_property', name]);
|
|
113
|
+
}
|
|
114
|
+
setProperty(name, value) {
|
|
115
|
+
return this.sendCommand(['set_property', name, value]);
|
|
116
|
+
}
|
|
117
|
+
sendCommand(command) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const id = ++this.requestId;
|
|
120
|
+
const socket = net.createConnection(SOCKET_PATH);
|
|
121
|
+
let buffer = '';
|
|
122
|
+
socket.on('connect', () => {
|
|
123
|
+
const msg = JSON.stringify({ command, request_id: id }) + '\n';
|
|
124
|
+
socket.write(msg);
|
|
125
|
+
});
|
|
126
|
+
socket.on('data', (data) => {
|
|
127
|
+
buffer += data.toString();
|
|
128
|
+
const lines = buffer.split('\n');
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (!line.trim())
|
|
131
|
+
continue;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(line);
|
|
134
|
+
if (parsed.request_id === id) {
|
|
135
|
+
socket.destroy();
|
|
136
|
+
if (parsed.error && parsed.error !== 'success') {
|
|
137
|
+
reject(new Error(parsed.error));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
resolve(parsed.data);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore unparseable lines (event messages, etc.)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
socket.on('error', (err) => {
|
|
151
|
+
reject(new Error(`mpv IPC connection failed: ${err.message}`));
|
|
152
|
+
});
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
socket.destroy();
|
|
155
|
+
reject(new Error('mpv IPC timeout'));
|
|
156
|
+
}, 3000);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export const mpvPlayer = new MpvPlayer();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface ApiResponse<T> {
|
|
2
|
+
success: boolean;
|
|
3
|
+
data: T | null;
|
|
4
|
+
error: ApiError | null;
|
|
5
|
+
}
|
|
6
|
+
export interface ApiError {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Artist {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Album {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
picUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Track {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
artists: Artist[];
|
|
23
|
+
album: Album;
|
|
24
|
+
duration: number;
|
|
25
|
+
uri: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Playlist {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
coverUrl?: string;
|
|
32
|
+
trackCount: number;
|
|
33
|
+
creator?: {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
tracks?: Track[];
|
|
38
|
+
}
|
|
39
|
+
export type SearchType = 'track' | 'album' | 'playlist' | 'artist';
|
|
40
|
+
export interface SearchResult {
|
|
41
|
+
tracks?: Track[];
|
|
42
|
+
albums?: Album[];
|
|
43
|
+
playlists?: Playlist[];
|
|
44
|
+
artists?: Artist[];
|
|
45
|
+
total: number;
|
|
46
|
+
offset: number;
|
|
47
|
+
limit: number;
|
|
48
|
+
}
|
|
49
|
+
export interface Lyric {
|
|
50
|
+
lrc?: string;
|
|
51
|
+
tlyric?: string;
|
|
52
|
+
}
|
|
53
|
+
export type Quality = 'standard' | 'higher' | 'exhigh' | 'lossless' | 'hires';
|
|
54
|
+
export interface UserProfile {
|
|
55
|
+
id: string;
|
|
56
|
+
nickname: string;
|
|
57
|
+
avatarUrl?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface CookieData {
|
|
60
|
+
MUSIC_U?: string;
|
|
61
|
+
[key: string]: string | undefined;
|
|
62
|
+
}
|
|
63
|
+
export declare const ExitCode: {
|
|
64
|
+
readonly SUCCESS: 0;
|
|
65
|
+
readonly GENERAL_ERROR: 1;
|
|
66
|
+
readonly AUTH_ERROR: 2;
|
|
67
|
+
readonly NETWORK_ERROR: 3;
|
|
68
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neteasecli",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Simple CLI for Netease Cloud Music",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"neteasecli": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.0.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsx src/index.ts",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"lint": "eslint src/",
|
|
23
|
+
"lint:fix": "eslint src/ --fix",
|
|
24
|
+
"format": "prettier --write src/",
|
|
25
|
+
"format:check": "prettier --check src/",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"netease",
|
|
30
|
+
"netease-cloud-music",
|
|
31
|
+
"ncm",
|
|
32
|
+
"music",
|
|
33
|
+
"music-cli",
|
|
34
|
+
"cli",
|
|
35
|
+
"terminal",
|
|
36
|
+
"mpv",
|
|
37
|
+
"streaming",
|
|
38
|
+
"player",
|
|
39
|
+
"163music",
|
|
40
|
+
"wangyiyun"
|
|
41
|
+
],
|
|
42
|
+
"author": "wangwalk",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/wangwalk/neteasecli.git"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@steipete/sweet-cookie": "^0.1.0",
|
|
50
|
+
"axios": "^1.6.0",
|
|
51
|
+
"commander": "^12.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^9.39.2",
|
|
55
|
+
"@types/node": "^20.10.0",
|
|
56
|
+
"eslint": "^9.39.2",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"prettier": "^3.8.1",
|
|
59
|
+
"tsx": "^4.7.0",
|
|
60
|
+
"typescript": "^5.3.0",
|
|
61
|
+
"typescript-eslint": "^8.54.0"
|
|
62
|
+
}
|
|
63
|
+
}
|