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,155 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { mpvPlayer } from '../player/mpv.js';
|
|
3
|
+
import { output, outputError } from '../output/json.js';
|
|
4
|
+
import { ExitCode } from '../types/index.js';
|
|
5
|
+
function formatTime(seconds) {
|
|
6
|
+
const mins = Math.floor(seconds / 60);
|
|
7
|
+
const secs = Math.floor(seconds % 60);
|
|
8
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
9
|
+
}
|
|
10
|
+
async function requireRunning() {
|
|
11
|
+
if (!(await mpvPlayer.isRunning())) {
|
|
12
|
+
outputError('PLAYER_ERROR', 'Nothing is playing');
|
|
13
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createPlayerCommand() {
|
|
17
|
+
const player = new Command('player').description('Playback control');
|
|
18
|
+
player
|
|
19
|
+
.command('status')
|
|
20
|
+
.description('Current playback status')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const status = await mpvPlayer.getStatus();
|
|
24
|
+
if (!status.playing) {
|
|
25
|
+
output({ playing: false, message: 'Nothing is playing' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const repeat = status.loop !== 'no' && status.loop !== 'false';
|
|
29
|
+
output({
|
|
30
|
+
playing: true,
|
|
31
|
+
paused: status.paused,
|
|
32
|
+
title: status.title,
|
|
33
|
+
position: status.position,
|
|
34
|
+
duration: status.duration,
|
|
35
|
+
positionFormatted: formatTime(status.position),
|
|
36
|
+
durationFormatted: formatTime(status.duration),
|
|
37
|
+
volume: Math.round(status.volume),
|
|
38
|
+
repeat,
|
|
39
|
+
message: `${status.paused ? '⏸' : '▶'} ${status.title || 'Unknown'} ${formatTime(status.position)}/${formatTime(status.duration)} vol:${Math.round(status.volume)}%${repeat ? ' 🔁' : ''}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
44
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
player
|
|
48
|
+
.command('pause')
|
|
49
|
+
.description('Toggle pause/resume')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
try {
|
|
52
|
+
await requireRunning();
|
|
53
|
+
await mpvPlayer.pause();
|
|
54
|
+
const status = await mpvPlayer.getStatus();
|
|
55
|
+
output({
|
|
56
|
+
paused: status.paused,
|
|
57
|
+
message: status.paused ? 'Paused' : 'Resumed',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
62
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
player
|
|
66
|
+
.command('stop')
|
|
67
|
+
.description('Stop playback')
|
|
68
|
+
.action(async () => {
|
|
69
|
+
try {
|
|
70
|
+
await mpvPlayer.stop();
|
|
71
|
+
output({ message: 'Stopped' });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
75
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
player
|
|
79
|
+
.command('seek <seconds>')
|
|
80
|
+
.description('Seek by relative seconds (e.g. 10, -10) or absolute with --absolute')
|
|
81
|
+
.option('--absolute', 'Seek to absolute position')
|
|
82
|
+
.action(async (seconds, opts) => {
|
|
83
|
+
try {
|
|
84
|
+
await requireRunning();
|
|
85
|
+
const secs = Number(seconds);
|
|
86
|
+
if (isNaN(secs)) {
|
|
87
|
+
outputError('PLAYER_ERROR', 'Invalid seconds value');
|
|
88
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await mpvPlayer.seek(secs, opts.absolute ? 'absolute' : 'relative');
|
|
92
|
+
const status = await mpvPlayer.getStatus();
|
|
93
|
+
output({
|
|
94
|
+
position: status.position,
|
|
95
|
+
duration: status.duration,
|
|
96
|
+
positionFormatted: formatTime(status.position),
|
|
97
|
+
durationFormatted: formatTime(status.duration),
|
|
98
|
+
message: `Seeked to ${formatTime(status.position)}/${formatTime(status.duration)}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
103
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
player
|
|
107
|
+
.command('volume [level]')
|
|
108
|
+
.description('Get or set volume (0-150)')
|
|
109
|
+
.action(async (level) => {
|
|
110
|
+
try {
|
|
111
|
+
await requireRunning();
|
|
112
|
+
if (level !== undefined) {
|
|
113
|
+
const vol = Number(level);
|
|
114
|
+
if (isNaN(vol)) {
|
|
115
|
+
outputError('PLAYER_ERROR', 'Invalid volume value');
|
|
116
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await mpvPlayer.setVolume(vol);
|
|
120
|
+
output({ volume: vol, message: `Volume: ${vol}%` });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const vol = await mpvPlayer.getVolume();
|
|
124
|
+
output({ volume: vol, message: `Volume: ${Math.round(vol)}%` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
129
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
player
|
|
133
|
+
.command('repeat [mode]')
|
|
134
|
+
.description('Toggle or set repeat mode (off/on)')
|
|
135
|
+
.action(async (mode) => {
|
|
136
|
+
try {
|
|
137
|
+
await requireRunning();
|
|
138
|
+
if (mode !== undefined) {
|
|
139
|
+
await mpvPlayer.setLoop(mode === 'on' ? 'inf' : 'no');
|
|
140
|
+
output({ repeat: mode === 'on', message: `Repeat: ${mode}` });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const current = await mpvPlayer.getLoop();
|
|
144
|
+
const isOn = current !== 'no' && current !== 'false';
|
|
145
|
+
await mpvPlayer.setLoop(isOn ? 'no' : 'inf');
|
|
146
|
+
output({ repeat: !isOn, message: `Repeat: ${!isOn ? 'on' : 'off'}` });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
151
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return player;
|
|
155
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getUserPlaylists, getPlaylistDetail } from '../api/playlist.js';
|
|
3
|
+
import { output, outputError } from '../output/json.js';
|
|
4
|
+
import { ExitCode } from '../types/index.js';
|
|
5
|
+
export function createPlaylistCommand() {
|
|
6
|
+
const playlist = new Command('playlist').description('Playlists');
|
|
7
|
+
playlist
|
|
8
|
+
.command('list')
|
|
9
|
+
.description('List my playlists')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const playlists = await getUserPlaylists();
|
|
13
|
+
output({
|
|
14
|
+
playlists: playlists.map((p) => ({
|
|
15
|
+
id: p.id,
|
|
16
|
+
name: p.name,
|
|
17
|
+
trackCount: p.trackCount,
|
|
18
|
+
creator: p.creator?.name,
|
|
19
|
+
})),
|
|
20
|
+
total: playlists.length,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
outputError('PLAYLIST_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
25
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
playlist
|
|
29
|
+
.command('detail')
|
|
30
|
+
.description('Playlist details')
|
|
31
|
+
.argument('<id>', 'Playlist ID')
|
|
32
|
+
.option('-l, --limit <number>', 'Track count limit', '50')
|
|
33
|
+
.action(async (id, options) => {
|
|
34
|
+
try {
|
|
35
|
+
const detail = await getPlaylistDetail(id);
|
|
36
|
+
const limit = parseInt(options.limit);
|
|
37
|
+
output({
|
|
38
|
+
id: detail.id,
|
|
39
|
+
name: detail.name,
|
|
40
|
+
description: detail.description,
|
|
41
|
+
coverUrl: detail.coverUrl,
|
|
42
|
+
trackCount: detail.trackCount,
|
|
43
|
+
creator: detail.creator,
|
|
44
|
+
tracks: detail.tracks?.slice(0, limit).map((t) => ({
|
|
45
|
+
id: t.id,
|
|
46
|
+
name: t.name,
|
|
47
|
+
artist: t.artists.map((a) => a.name).join(', '),
|
|
48
|
+
album: t.album.name,
|
|
49
|
+
duration: t.duration,
|
|
50
|
+
uri: t.uri,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
outputError('PLAYLIST_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
56
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return playlist;
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { search } from '../api/search.js';
|
|
3
|
+
import { output, outputError } from '../output/json.js';
|
|
4
|
+
import { ExitCode } from '../types/index.js';
|
|
5
|
+
export function createSearchCommand() {
|
|
6
|
+
const searchCmd = new Command('search').description('Search music');
|
|
7
|
+
const createSubCommand = (type, description) => {
|
|
8
|
+
return new Command(type)
|
|
9
|
+
.description(description)
|
|
10
|
+
.argument('<keyword>', 'Search keyword')
|
|
11
|
+
.option('-l, --limit <number>', 'Result count', '20')
|
|
12
|
+
.option('-o, --offset <number>', 'Offset', '0')
|
|
13
|
+
.action(async (keyword, options) => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await search(keyword, type, parseInt(options.limit), parseInt(options.offset));
|
|
16
|
+
output(result);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
outputError('SEARCH_ERROR', error instanceof Error ? error.message : 'Search failed');
|
|
20
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
searchCmd.addCommand(createSubCommand('track', 'Search tracks'));
|
|
25
|
+
searchCmd.addCommand(createSubCommand('album', 'Search albums'));
|
|
26
|
+
searchCmd.addCommand(createSubCommand('playlist', 'Search playlists'));
|
|
27
|
+
searchCmd.addCommand(createSubCommand('artist', 'Search artists'));
|
|
28
|
+
return searchCmd;
|
|
29
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getTrackDetail, getTrackUrl, getLyric, downloadTrack } from '../api/track.js';
|
|
3
|
+
import { mpvPlayer } from '../player/mpv.js';
|
|
4
|
+
import { output, outputError } from '../output/json.js';
|
|
5
|
+
import { ExitCode } from '../types/index.js';
|
|
6
|
+
export function createTrackCommand() {
|
|
7
|
+
const track = new Command('track').description('Track info');
|
|
8
|
+
track
|
|
9
|
+
.command('detail')
|
|
10
|
+
.description('Track details')
|
|
11
|
+
.argument('<id>', 'Track ID')
|
|
12
|
+
.action(async (id) => {
|
|
13
|
+
try {
|
|
14
|
+
const detail = await getTrackDetail(id);
|
|
15
|
+
output({
|
|
16
|
+
id: detail.id,
|
|
17
|
+
name: detail.name,
|
|
18
|
+
artists: detail.artists,
|
|
19
|
+
album: detail.album,
|
|
20
|
+
duration: detail.duration,
|
|
21
|
+
durationFormatted: formatDuration(detail.duration),
|
|
22
|
+
uri: detail.uri,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
outputError('TRACK_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
27
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
track
|
|
31
|
+
.command('url')
|
|
32
|
+
.description('Get streaming URL')
|
|
33
|
+
.argument('<id>', 'Track ID')
|
|
34
|
+
.option('-q, --quality <level>', 'Quality: standard/higher/exhigh/lossless/hires', 'exhigh')
|
|
35
|
+
.action(async (id, options) => {
|
|
36
|
+
try {
|
|
37
|
+
const url = await getTrackUrl(id, options.quality);
|
|
38
|
+
output({ id, url, quality: options.quality });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
outputError('TRACK_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
42
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
track
|
|
46
|
+
.command('lyric')
|
|
47
|
+
.description('Get lyrics')
|
|
48
|
+
.argument('<id>', 'Track ID')
|
|
49
|
+
.action(async (id) => {
|
|
50
|
+
try {
|
|
51
|
+
const lyric = await getLyric(id);
|
|
52
|
+
output({
|
|
53
|
+
id,
|
|
54
|
+
lrc: lyric.lrc,
|
|
55
|
+
tlyric: lyric.tlyric,
|
|
56
|
+
hasLyric: !!lyric.lrc,
|
|
57
|
+
hasTranslation: !!lyric.tlyric,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
outputError('TRACK_ERROR', error instanceof Error ? error.message : 'Failed');
|
|
62
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
track
|
|
66
|
+
.command('download')
|
|
67
|
+
.description('Download track')
|
|
68
|
+
.argument('<id>', 'Track ID')
|
|
69
|
+
.option('-q, --quality <level>', 'Quality: standard/higher/exhigh/lossless/hires', 'exhigh')
|
|
70
|
+
.option('-o, --output <path>', 'Output file path')
|
|
71
|
+
.action(async (id, options) => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await downloadTrack(id, options.quality, options.output);
|
|
74
|
+
output({
|
|
75
|
+
id,
|
|
76
|
+
path: result.path,
|
|
77
|
+
size: result.size,
|
|
78
|
+
quality: options.quality,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
outputError('TRACK_ERROR', error instanceof Error ? error.message : 'Download failed');
|
|
83
|
+
process.exit(ExitCode.NETWORK_ERROR);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
track
|
|
87
|
+
.command('play')
|
|
88
|
+
.description('Play track via mpv')
|
|
89
|
+
.argument('<id>', 'Track ID')
|
|
90
|
+
.option('-q, --quality <level>', 'Quality: standard/higher/exhigh/lossless/hires', 'exhigh')
|
|
91
|
+
.action(async (id, options) => {
|
|
92
|
+
try {
|
|
93
|
+
const [url, detail] = await Promise.all([
|
|
94
|
+
getTrackUrl(id, options.quality),
|
|
95
|
+
getTrackDetail(id),
|
|
96
|
+
]);
|
|
97
|
+
const title = `${detail.name} - ${detail.artists.map((a) => a.name).join('/')}`;
|
|
98
|
+
await mpvPlayer.play(url, title);
|
|
99
|
+
output({
|
|
100
|
+
id,
|
|
101
|
+
name: detail.name,
|
|
102
|
+
artists: detail.artists,
|
|
103
|
+
quality: options.quality,
|
|
104
|
+
message: `Now playing: ${title}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
outputError('PLAYER_ERROR', error instanceof Error ? error.message : 'Playback failed');
|
|
109
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return track;
|
|
113
|
+
}
|
|
114
|
+
function formatDuration(ms) {
|
|
115
|
+
const seconds = Math.floor(ms / 1000);
|
|
116
|
+
const minutes = Math.floor(seconds / 60);
|
|
117
|
+
const secs = seconds % 60;
|
|
118
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
119
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createProgram } from './cli/index.js';
|
|
3
|
+
const program = createProgram();
|
|
4
|
+
try {
|
|
5
|
+
await program.parseAsync();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
console.error(JSON.stringify({
|
|
10
|
+
success: false,
|
|
11
|
+
error: {
|
|
12
|
+
code: 'CLI_ERROR',
|
|
13
|
+
message: error.message,
|
|
14
|
+
},
|
|
15
|
+
}, null, 2));
|
|
16
|
+
}
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function setNoColor(v: boolean): void;
|
|
2
|
+
export declare const bold: (t: string) => string;
|
|
3
|
+
export declare const dim: (t: string) => string;
|
|
4
|
+
export declare const green: (t: string) => string;
|
|
5
|
+
export declare const red: (t: string) => string;
|
|
6
|
+
export declare const yellow: (t: string) => string;
|
|
7
|
+
export declare const cyan: (t: string) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const envDisabled = !!process.env.NO_COLOR || process.env.TERM === 'dumb';
|
|
2
|
+
let forceOff = false;
|
|
3
|
+
export function setNoColor(v) {
|
|
4
|
+
forceOff = v;
|
|
5
|
+
}
|
|
6
|
+
function enabled() {
|
|
7
|
+
return !envDisabled && !forceOff && !!process.stdout.isTTY;
|
|
8
|
+
}
|
|
9
|
+
function wrap(code, text) {
|
|
10
|
+
return enabled() ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
11
|
+
}
|
|
12
|
+
export const bold = (t) => wrap(1, t);
|
|
13
|
+
export const dim = (t) => wrap(2, t);
|
|
14
|
+
export const green = (t) => wrap(32, t);
|
|
15
|
+
export const red = (t) => wrap(31, t);
|
|
16
|
+
export const yellow = (t) => wrap(33, t);
|
|
17
|
+
export const cyan = (t) => wrap(36, t);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type OutputMode = 'json' | 'plain' | 'human';
|
|
2
|
+
export declare function setOutputMode(m: OutputMode): void;
|
|
3
|
+
export declare function setPrettyPrint(value: boolean): void;
|
|
4
|
+
export declare function setQuietMode(value: boolean): void;
|
|
5
|
+
export declare function output<T>(data: T): void;
|
|
6
|
+
export declare function outputError(code: string, message: string): void;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { bold, dim, green, red, cyan, yellow } from './color.js';
|
|
2
|
+
let mode = process.stdout.isTTY ? 'human' : 'json';
|
|
3
|
+
let prettyPrint = false;
|
|
4
|
+
let quietMode = false;
|
|
5
|
+
export function setOutputMode(m) {
|
|
6
|
+
mode = m;
|
|
7
|
+
}
|
|
8
|
+
export function setPrettyPrint(value) {
|
|
9
|
+
prettyPrint = value;
|
|
10
|
+
}
|
|
11
|
+
export function setQuietMode(value) {
|
|
12
|
+
quietMode = value;
|
|
13
|
+
}
|
|
14
|
+
export function output(data) {
|
|
15
|
+
if (quietMode)
|
|
16
|
+
return;
|
|
17
|
+
switch (mode) {
|
|
18
|
+
case 'json':
|
|
19
|
+
outputJson(data);
|
|
20
|
+
break;
|
|
21
|
+
case 'plain':
|
|
22
|
+
outputPlain(data);
|
|
23
|
+
break;
|
|
24
|
+
case 'human':
|
|
25
|
+
outputHuman(data);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function outputError(code, message) {
|
|
30
|
+
if (quietMode)
|
|
31
|
+
return;
|
|
32
|
+
switch (mode) {
|
|
33
|
+
case 'json': {
|
|
34
|
+
const response = {
|
|
35
|
+
success: false,
|
|
36
|
+
data: null,
|
|
37
|
+
error: { code, message },
|
|
38
|
+
};
|
|
39
|
+
console.log(prettyPrint ? JSON.stringify(response, null, 2) : JSON.stringify(response));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'plain':
|
|
43
|
+
console.log(`error\t${code}\t${message}`);
|
|
44
|
+
break;
|
|
45
|
+
case 'human':
|
|
46
|
+
console.log(`${red('✗')} ${bold(code)}: ${message}`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// --- JSON mode ---
|
|
51
|
+
function outputJson(data) {
|
|
52
|
+
const response = { success: true, data, error: null };
|
|
53
|
+
console.log(prettyPrint ? JSON.stringify(response, null, 2) : JSON.stringify(response));
|
|
54
|
+
}
|
|
55
|
+
// --- Plain mode ---
|
|
56
|
+
function outputPlain(data) {
|
|
57
|
+
const d = data;
|
|
58
|
+
if (d.tracks && Array.isArray(d.tracks)) {
|
|
59
|
+
for (const t of d.tracks) {
|
|
60
|
+
console.log([t.id, t.name, t.artist || formatArtists(t.artists), t.album, t.uri].join('\t'));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (d.playlists && Array.isArray(d.playlists)) {
|
|
65
|
+
for (const p of d.playlists) {
|
|
66
|
+
console.log([p.id, p.name, p.trackCount, p.creator].join('\t'));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (d.lrc !== undefined) {
|
|
71
|
+
if (d.lrc)
|
|
72
|
+
console.log(String(d.lrc));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (d.url !== undefined) {
|
|
76
|
+
console.log(String(d.url));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (d.message !== undefined) {
|
|
80
|
+
console.log(String(d.message));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Fallback: key=value
|
|
84
|
+
for (const [k, v] of Object.entries(d)) {
|
|
85
|
+
if (v !== undefined && v !== null && typeof v !== 'object') {
|
|
86
|
+
console.log(`${k}\t${v}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// --- Human mode ---
|
|
91
|
+
function outputHuman(data) {
|
|
92
|
+
const d = data;
|
|
93
|
+
// Track list (search results, library, playlist detail)
|
|
94
|
+
if (d.tracks && Array.isArray(d.tracks)) {
|
|
95
|
+
const tracks = d.tracks;
|
|
96
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
97
|
+
const t = tracks[i];
|
|
98
|
+
const artist = t.artist || formatArtists(t.artists);
|
|
99
|
+
const dur = t.duration ? ` ${dim(formatDuration(Number(t.duration)))}` : '';
|
|
100
|
+
console.log(` ${dim(String(i + 1).padStart(2, ' '))} ${bold(String(t.name))} ${dim('-')} ${cyan(String(artist))}${dur}`);
|
|
101
|
+
}
|
|
102
|
+
const total = d.total !== undefined ? Number(d.total) : tracks.length;
|
|
103
|
+
const showing = d.showing !== undefined ? Number(d.showing) : tracks.length;
|
|
104
|
+
if (total > showing) {
|
|
105
|
+
console.log(dim(`\n ${showing} of ${total} tracks`));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Playlist list
|
|
110
|
+
if (d.playlists && Array.isArray(d.playlists)) {
|
|
111
|
+
const pls = d.playlists;
|
|
112
|
+
for (let i = 0; i < pls.length; i++) {
|
|
113
|
+
const p = pls[i];
|
|
114
|
+
console.log(` ${dim(String(i + 1).padStart(2, ' '))} ${bold(String(p.name))} ${dim(`(${p.trackCount} tracks)`)}`);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Lyrics
|
|
119
|
+
if (d.lrc !== undefined) {
|
|
120
|
+
if (d.lrc) {
|
|
121
|
+
console.log(String(d.lrc));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(dim('No lyrics available'));
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Single track detail
|
|
129
|
+
if (d.artists && Array.isArray(d.artists) && d.album && d.duration) {
|
|
130
|
+
console.log(` ${bold(String(d.name))}`);
|
|
131
|
+
console.log(` ${dim('Artist:')} ${cyan(formatArtists(d.artists))}`);
|
|
132
|
+
const album = d.album;
|
|
133
|
+
console.log(` ${dim('Album:')} ${String(album.name)}`);
|
|
134
|
+
console.log(` ${dim('Duration:')} ${d.durationFormatted || formatDuration(Number(d.duration))}`);
|
|
135
|
+
if (d.uri)
|
|
136
|
+
console.log(` ${dim('URI:')} ${String(d.uri)}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// URL result
|
|
140
|
+
if (d.url !== undefined && d.quality !== undefined) {
|
|
141
|
+
console.log(` ${dim('URL:')} ${String(d.url)}`);
|
|
142
|
+
console.log(` ${dim('Quality:')} ${String(d.quality)}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Auth check result
|
|
146
|
+
if (d.credentials !== undefined && typeof d.credentials === 'object') {
|
|
147
|
+
const creds = d.credentials;
|
|
148
|
+
const warnings = d.warnings || [];
|
|
149
|
+
const profile = d.profile ? String(d.profile) : 'default';
|
|
150
|
+
console.log(`${bold('Credential check')} ${dim(`(profile: ${profile})`)}`);
|
|
151
|
+
console.log(dim('─'.repeat(40)));
|
|
152
|
+
for (const [name, found] of Object.entries(creds)) {
|
|
153
|
+
const icon = found ? green('✓') : red('✗');
|
|
154
|
+
const status = found ? 'found' : 'not found';
|
|
155
|
+
console.log(`${icon} ${bold(name)}: ${found ? green(status) : red(status)}`);
|
|
156
|
+
}
|
|
157
|
+
if (d.valid && d.nickname) {
|
|
158
|
+
console.log(`${green('✓')} ${bold('session')}: ${green('valid')} ${dim(`(${d.nickname})`)}`);
|
|
159
|
+
}
|
|
160
|
+
else if (creds.MUSIC_U) {
|
|
161
|
+
console.log(`${red('✗')} ${bold('session')}: ${red('expired or invalid')}`);
|
|
162
|
+
}
|
|
163
|
+
if (warnings.length > 0) {
|
|
164
|
+
console.log(`\n${yellow('⚠')} ${bold('Warnings:')}`);
|
|
165
|
+
for (const w of warnings) {
|
|
166
|
+
console.log(` ${dim('-')} ${w}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!d.valid) {
|
|
170
|
+
console.log(`\n${red('✗')} Missing credentials. Options:`);
|
|
171
|
+
console.log(` 1. Login to ${cyan('music.163.com')} in Chrome, Edge, Firefox, or Safari`);
|
|
172
|
+
console.log(` 2. Run ${cyan('neteasecli auth login')}`);
|
|
173
|
+
console.log(` 3. Use ${cyan('--profile <name>')} for a specific Chrome/Edge profile`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Message-based output (auth, player, etc.)
|
|
178
|
+
if (d.message !== undefined) {
|
|
179
|
+
const icon = green('✓');
|
|
180
|
+
console.log(`${icon} ${String(d.message)}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Fallback
|
|
184
|
+
for (const [k, v] of Object.entries(d)) {
|
|
185
|
+
if (v !== undefined && v !== null && typeof v !== 'object') {
|
|
186
|
+
console.log(` ${dim(k + ':')} ${v}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// --- Helpers ---
|
|
191
|
+
function formatArtists(artists) {
|
|
192
|
+
if (!Array.isArray(artists))
|
|
193
|
+
return String(artists || '');
|
|
194
|
+
return artists.map((a) => a.name || a).join(', ');
|
|
195
|
+
}
|
|
196
|
+
function formatDuration(ms) {
|
|
197
|
+
const seconds = Math.floor(ms / 1000);
|
|
198
|
+
const minutes = Math.floor(seconds / 60);
|
|
199
|
+
const secs = seconds % 60;
|
|
200
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
201
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function setVerbose(v: boolean): void;
|
|
2
|
+
export declare function setDebug(v: boolean): void;
|
|
3
|
+
export declare function isVerbose(): boolean;
|
|
4
|
+
export declare function isDebug(): boolean;
|
|
5
|
+
export declare function verbose(msg: string): void;
|
|
6
|
+
export declare function debug(msg: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dim, yellow } from './color.js';
|
|
2
|
+
let verboseEnabled = false;
|
|
3
|
+
let debugEnabled = false;
|
|
4
|
+
export function setVerbose(v) {
|
|
5
|
+
verboseEnabled = v;
|
|
6
|
+
}
|
|
7
|
+
export function setDebug(v) {
|
|
8
|
+
debugEnabled = v;
|
|
9
|
+
if (v)
|
|
10
|
+
verboseEnabled = true;
|
|
11
|
+
}
|
|
12
|
+
export function isVerbose() {
|
|
13
|
+
return verboseEnabled;
|
|
14
|
+
}
|
|
15
|
+
export function isDebug() {
|
|
16
|
+
return debugEnabled;
|
|
17
|
+
}
|
|
18
|
+
export function verbose(msg) {
|
|
19
|
+
if (verboseEnabled)
|
|
20
|
+
process.stderr.write(dim(`[verbose] ${msg}`) + '\n');
|
|
21
|
+
}
|
|
22
|
+
export function debug(msg) {
|
|
23
|
+
if (debugEnabled)
|
|
24
|
+
process.stderr.write(yellow(`[debug] ${msg}`) + '\n');
|
|
25
|
+
}
|