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
@@ -0,0 +1,96 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import { getApiClient } from './client.js';
4
+ const qualityBrMap = {
5
+ standard: 128000,
6
+ higher: 192000,
7
+ exhigh: 320000,
8
+ lossless: 999000,
9
+ hires: 999000,
10
+ };
11
+ export async function getTrackDetail(id) {
12
+ const client = getApiClient();
13
+ const response = await client.request('/song/detail', {
14
+ c: JSON.stringify([{ id: Number(id) }]),
15
+ ids: `[${id}]`,
16
+ });
17
+ if (!response.songs || response.songs.length === 0) {
18
+ throw new Error(`Track not found: ${id}`);
19
+ }
20
+ const track = response.songs[0];
21
+ const artists = track.ar || track.artists || [];
22
+ const album = track.al || track.album || { id: 0, name: '' };
23
+ return {
24
+ id: String(track.id),
25
+ name: track.name,
26
+ artists: artists.map((a) => ({ id: String(a.id), name: a.name })),
27
+ album: {
28
+ id: String(album.id),
29
+ name: album.name,
30
+ picUrl: album.picUrl,
31
+ },
32
+ duration: track.dt || track.duration || 0,
33
+ uri: `netease:track:${track.id}`,
34
+ };
35
+ }
36
+ export async function getTrackUrl(id, quality = 'exhigh') {
37
+ const client = getApiClient();
38
+ const br = qualityBrMap[quality];
39
+ const response = await client.request('/song/enhance/player/url', {
40
+ ids: `[${id}]`,
41
+ br,
42
+ });
43
+ if (!response.data || response.data.length === 0) {
44
+ throw new Error(`Cannot get track URL: ${id}`);
45
+ }
46
+ const url = response.data[0].url;
47
+ if (!url) {
48
+ throw new Error('Track unavailable (no copyright or VIP required)');
49
+ }
50
+ return url;
51
+ }
52
+ export async function getLyric(id) {
53
+ const client = getApiClient();
54
+ const response = await client.request('/song/lyric', {
55
+ id,
56
+ lv: -1,
57
+ tv: -1,
58
+ });
59
+ return {
60
+ lrc: response.lrc?.lyric,
61
+ tlyric: response.tlyric?.lyric,
62
+ };
63
+ }
64
+ export async function downloadTrack(id, quality = 'exhigh', outputPath) {
65
+ const client = getApiClient();
66
+ const url = await getTrackUrl(id, quality);
67
+ const ext = url.includes('.flac') ? 'flac' : 'mp3';
68
+ const dest = outputPath || path.join(os.tmpdir(), `neteasecli-${id}.${ext}`);
69
+ await client.download(url, dest);
70
+ const { size } = await import('fs').then((fs) => fs.statSync(dest));
71
+ return { path: dest, size };
72
+ }
73
+ export async function getTrackDetails(ids) {
74
+ const client = getApiClient();
75
+ const c = JSON.stringify(ids.map((id) => ({ id: Number(id) })));
76
+ const response = await client.request('/song/detail', {
77
+ c,
78
+ ids: `[${ids.join(',')}]`,
79
+ });
80
+ return (response.songs || []).map((track) => {
81
+ const artists = track.ar || track.artists || [];
82
+ const album = track.al || track.album || { id: 0, name: '' };
83
+ return {
84
+ id: String(track.id),
85
+ name: track.name,
86
+ artists: artists.map((a) => ({ id: String(a.id), name: a.name })),
87
+ album: {
88
+ id: String(album.id),
89
+ name: album.name,
90
+ picUrl: album.picUrl,
91
+ },
92
+ duration: track.dt || track.duration || 0,
93
+ uri: `netease:track:${track.id}`,
94
+ };
95
+ });
96
+ }
@@ -0,0 +1,5 @@
1
+ import type { Track, UserProfile } from '../types/index.js';
2
+ export declare function getUserProfile(): Promise<UserProfile>;
3
+ export declare function getLikedTrackIds(): Promise<string[]>;
4
+ export declare function likeTrack(id: string, like?: boolean): Promise<void>;
5
+ export declare function getRecentTracks(limit?: number): Promise<Track[]>;
@@ -0,0 +1,50 @@
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 getUserProfile() {
19
+ const client = getApiClient();
20
+ const response = await client.request('/nuser/account/get');
21
+ return {
22
+ id: String(response.profile.userId),
23
+ nickname: response.profile.nickname,
24
+ avatarUrl: response.profile.avatarUrl,
25
+ };
26
+ }
27
+ export async function getLikedTrackIds() {
28
+ const client = getApiClient();
29
+ const userProfile = await getUserProfile();
30
+ const response = await client.request('/song/like/get', {
31
+ uid: userProfile.id,
32
+ });
33
+ return response.ids.map(String);
34
+ }
35
+ export async function likeTrack(id, like = true) {
36
+ const client = getApiClient();
37
+ await client.request('/like', {
38
+ trackId: id,
39
+ like,
40
+ });
41
+ }
42
+ export async function getRecentTracks(limit = 100) {
43
+ const client = getApiClient();
44
+ const response = await client.request('/play/record', {
45
+ uid: (await getUserProfile()).id,
46
+ type: 0,
47
+ limit,
48
+ });
49
+ return (response.allData || []).map((item) => transformTrack(item.song));
50
+ }
@@ -0,0 +1,24 @@
1
+ import type { CookieData } from '../types/index.js';
2
+ export declare class AuthManager {
3
+ private cookies;
4
+ private cookieSource;
5
+ constructor();
6
+ importFromBrowser(profile?: string): Promise<void>;
7
+ checkAuth(): Promise<{
8
+ valid: boolean;
9
+ userId?: string;
10
+ nickname?: string;
11
+ error?: string;
12
+ credentials: {
13
+ MUSIC_U: boolean;
14
+ };
15
+ warnings: string[];
16
+ }>;
17
+ isAuthenticated(): boolean;
18
+ getCookies(): CookieData | null;
19
+ getCookieString(): string;
20
+ getSource(): string | undefined;
21
+ logout(): void;
22
+ }
23
+ export declare function getAuthManager(): AuthManager;
24
+ export declare function resetAuthManager(): void;
@@ -0,0 +1,108 @@
1
+ import { saveCookies, loadCookies, clearCookies } from './storage.js';
2
+ export class AuthManager {
3
+ cookies = null;
4
+ cookieSource;
5
+ constructor() {
6
+ this.cookies = loadCookies();
7
+ }
8
+ async importFromBrowser(profile) {
9
+ const { getCookies } = await import('@steipete/sweet-cookie');
10
+ const options = {
11
+ url: 'https://music.163.com/',
12
+ names: ['MUSIC_U'],
13
+ };
14
+ if (profile) {
15
+ options.chromeProfile = profile;
16
+ }
17
+ const { cookies, warnings } = await getCookies(options);
18
+ const cookieData = {};
19
+ let source;
20
+ for (const cookie of cookies) {
21
+ cookieData[cookie.name] = cookie.value;
22
+ if (!source && cookie.source?.browser) {
23
+ source = cookie.source.browser;
24
+ }
25
+ }
26
+ if (!cookieData.MUSIC_U) {
27
+ const parts = ['Could not find Netease login cookies.'];
28
+ if (warnings.length > 0) {
29
+ parts.push('', 'Warnings:');
30
+ for (const w of warnings) {
31
+ parts.push(` - ${w}`);
32
+ }
33
+ }
34
+ parts.push('', 'Options:', ' 1. Login to music.163.com in Chrome, Edge, Firefox, or Safari', ' 2. Run `neteasecli auth login`', ' 3. Use --profile <name> for a specific Chrome profile');
35
+ throw new Error(parts.join('\n'));
36
+ }
37
+ if (warnings.length > 0) {
38
+ const { verbose } = await import('../output/logger.js');
39
+ for (const w of warnings) {
40
+ verbose(`Cookie import warning: ${w}`);
41
+ }
42
+ }
43
+ this.cookies = cookieData;
44
+ this.cookieSource = source;
45
+ saveCookies(cookieData);
46
+ }
47
+ async checkAuth() {
48
+ const credentials = {
49
+ MUSIC_U: !!this.cookies?.MUSIC_U,
50
+ };
51
+ const warnings = [];
52
+ if (!this.cookies) {
53
+ warnings.push('No session file found. Run `neteasecli auth login` to import cookies from your browser.');
54
+ return { valid: false, error: 'Not logged in', credentials, warnings };
55
+ }
56
+ if (!credentials.MUSIC_U) {
57
+ warnings.push('Missing MUSIC_U cookie — login session not found');
58
+ return { valid: false, error: 'Missing credentials', credentials, warnings };
59
+ }
60
+ try {
61
+ const { getUserProfile } = await import('../api/user.js');
62
+ const profile = await getUserProfile();
63
+ return {
64
+ valid: true,
65
+ userId: profile.id,
66
+ nickname: profile.nickname,
67
+ credentials,
68
+ warnings,
69
+ };
70
+ }
71
+ catch (error) {
72
+ const msg = error instanceof Error ? error.message : 'Session expired';
73
+ warnings.push(`Session validation failed: ${msg}`);
74
+ return { valid: false, error: msg, credentials, warnings };
75
+ }
76
+ }
77
+ isAuthenticated() {
78
+ return this.cookies !== null && !!this.cookies.MUSIC_U;
79
+ }
80
+ getCookies() {
81
+ return this.cookies;
82
+ }
83
+ getCookieString() {
84
+ if (!this.cookies)
85
+ return '';
86
+ return Object.entries(this.cookies)
87
+ .filter(([, v]) => v !== undefined)
88
+ .map(([k, v]) => `${k}=${v}`)
89
+ .join('; ');
90
+ }
91
+ getSource() {
92
+ return this.cookieSource;
93
+ }
94
+ logout() {
95
+ this.cookies = null;
96
+ clearCookies();
97
+ }
98
+ }
99
+ let authManagerInstance = null;
100
+ export function getAuthManager() {
101
+ if (!authManagerInstance) {
102
+ authManagerInstance = new AuthManager();
103
+ }
104
+ return authManagerInstance;
105
+ }
106
+ export function resetAuthManager() {
107
+ authManagerInstance = null;
108
+ }
@@ -0,0 +1,7 @@
1
+ import type { CookieData } from '../types/index.js';
2
+ export declare function setProfile(name: string): void;
3
+ export declare function getProfile(): string;
4
+ export declare function saveCookies(cookies: CookieData): void;
5
+ export declare function loadCookies(): CookieData | null;
6
+ export declare function clearCookies(): void;
7
+ export declare function listProfiles(): string[];
@@ -0,0 +1,73 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ const BASE_CONFIG_DIR = path.join(os.homedir(), '.config', 'neteasecli');
5
+ let currentProfile = 'default';
6
+ export function setProfile(name) {
7
+ if (name !== currentProfile) {
8
+ currentProfile = name;
9
+ // Invalidate cached auth manager so it reloads cookies for new profile
10
+ import('../auth/manager.js').then((m) => m.resetAuthManager());
11
+ }
12
+ }
13
+ export function getProfile() {
14
+ return currentProfile;
15
+ }
16
+ function getProfileDir() {
17
+ return path.join(BASE_CONFIG_DIR, 'profiles', currentProfile);
18
+ }
19
+ function getSessionFile() {
20
+ return path.join(getProfileDir(), 'session.json');
21
+ }
22
+ function ensureProfileDir() {
23
+ const dir = getProfileDir();
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ }
27
+ }
28
+ // Migrate old flat session.json to default profile
29
+ function migrateIfNeeded() {
30
+ const oldFile = path.join(BASE_CONFIG_DIR, 'session.json');
31
+ const newFile = path.join(BASE_CONFIG_DIR, 'profiles', 'default', 'session.json');
32
+ if (fs.existsSync(oldFile) && !fs.existsSync(newFile)) {
33
+ const dir = path.dirname(newFile);
34
+ if (!fs.existsSync(dir)) {
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ }
37
+ fs.renameSync(oldFile, newFile);
38
+ }
39
+ }
40
+ // Run migration on module load
41
+ migrateIfNeeded();
42
+ export function saveCookies(cookies) {
43
+ ensureProfileDir();
44
+ fs.writeFileSync(getSessionFile(), JSON.stringify(cookies, null, 2));
45
+ }
46
+ export function loadCookies() {
47
+ const file = getSessionFile();
48
+ if (!fs.existsSync(file)) {
49
+ return null;
50
+ }
51
+ try {
52
+ const content = fs.readFileSync(file, 'utf-8');
53
+ return JSON.parse(content);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export function clearCookies() {
60
+ const file = getSessionFile();
61
+ if (fs.existsSync(file)) {
62
+ fs.unlinkSync(file);
63
+ }
64
+ }
65
+ export function listProfiles() {
66
+ const profilesDir = path.join(BASE_CONFIG_DIR, 'profiles');
67
+ if (!fs.existsSync(profilesDir))
68
+ return [];
69
+ return fs.readdirSync(profilesDir).filter((name) => {
70
+ const sessionFile = path.join(profilesDir, name, 'session.json');
71
+ return fs.existsSync(sessionFile);
72
+ });
73
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createAuthCommand(): Command;
@@ -0,0 +1,62 @@
1
+ import { Command } from 'commander';
2
+ import { getAuthManager } from '../auth/manager.js';
3
+ import { output, outputError } from '../output/json.js';
4
+ import { ExitCode } from '../types/index.js';
5
+ import { getProfile } from '../auth/storage.js';
6
+ export function createAuthCommand() {
7
+ const auth = new Command('auth').description('Authentication');
8
+ auth
9
+ .command('login')
10
+ .description('Import login cookies from browser (Chrome, Edge, Firefox, Safari)')
11
+ .option('--profile <name>', 'Chrome/Edge profile name')
12
+ .action(async (options) => {
13
+ const authManager = getAuthManager();
14
+ try {
15
+ await authManager.importFromBrowser(options.profile);
16
+ const source = authManager.getSource();
17
+ output({
18
+ message: `Login successful${source ? ` (via ${source})` : ''}`,
19
+ authenticated: true,
20
+ browser: source || 'unknown',
21
+ });
22
+ }
23
+ catch (error) {
24
+ outputError('AUTH_ERROR', error instanceof Error ? error.message : 'Login failed');
25
+ process.exit(ExitCode.AUTH_ERROR);
26
+ }
27
+ });
28
+ auth
29
+ .command('check')
30
+ .description('Check login status')
31
+ .action(async () => {
32
+ const authManager = getAuthManager();
33
+ try {
34
+ const result = await authManager.checkAuth();
35
+ output({
36
+ valid: result.valid,
37
+ userId: result.userId,
38
+ nickname: result.nickname,
39
+ profile: getProfile(),
40
+ credentials: result.credentials,
41
+ warnings: result.warnings,
42
+ error: result.error,
43
+ message: result.valid
44
+ ? `Logged in: ${result.nickname} (${result.userId})`
45
+ : result.error || 'Not logged in',
46
+ });
47
+ }
48
+ catch (error) {
49
+ outputError('AUTH_ERROR', error instanceof Error ? error.message : 'Check failed');
50
+ process.exit(ExitCode.AUTH_ERROR);
51
+ }
52
+ });
53
+ auth
54
+ .command('logout')
55
+ .description('Logout')
56
+ .action(() => {
57
+ const authManager = getAuthManager();
58
+ authManager.logout();
59
+ output({ message: 'Logged out', authenticated: false });
60
+ });
61
+ return auth;
62
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createProgram(): Command;
@@ -0,0 +1,57 @@
1
+ import { Command } from 'commander';
2
+ import { createAuthCommand } from './auth.js';
3
+ import { createSearchCommand } from './search.js';
4
+ import { createPlaylistCommand } from './playlist.js';
5
+ import { createLibraryCommand } from './library.js';
6
+ import { createTrackCommand } from './track.js';
7
+ import { createPlayerCommand } from './player.js';
8
+ import { setOutputMode, setPrettyPrint, setQuietMode } from '../output/json.js';
9
+ import { setNoColor } from '../output/color.js';
10
+ import { setVerbose, setDebug } from '../output/logger.js';
11
+ import { setProfile } from '../auth/storage.js';
12
+ import { setRequestTimeout } from '../api/client.js';
13
+ export function createProgram() {
14
+ const program = new Command();
15
+ program
16
+ .name('neteasecli')
17
+ .description('Netease Cloud Music CLI')
18
+ .version('2.0.0')
19
+ .option('--json', 'JSON output (default when piped)')
20
+ .option('--plain', 'Plain text output')
21
+ .option('--pretty', 'Pretty-print JSON')
22
+ .option('--quiet', 'Suppress output')
23
+ .option('--no-color', 'Disable colors')
24
+ .option('--profile <name>', 'Account profile', 'default')
25
+ .option('-v, --verbose', 'Verbose output')
26
+ .option('-d, --debug', 'Debug output (implies --verbose)')
27
+ .option('--timeout <seconds>', 'Request timeout in seconds', '30')
28
+ .hook('preAction', (thisCommand) => {
29
+ const opts = thisCommand.opts();
30
+ if (opts.profile && opts.profile !== 'default') {
31
+ setProfile(opts.profile);
32
+ }
33
+ if (opts.json)
34
+ setOutputMode('json');
35
+ if (opts.plain)
36
+ setOutputMode('plain');
37
+ if (opts.pretty)
38
+ setPrettyPrint(true);
39
+ if (opts.quiet)
40
+ setQuietMode(true);
41
+ if (!opts.color)
42
+ setNoColor(true);
43
+ if (opts.verbose)
44
+ setVerbose(true);
45
+ if (opts.debug)
46
+ setDebug(true);
47
+ if (opts.timeout)
48
+ setRequestTimeout(Number(opts.timeout) * 1000);
49
+ });
50
+ program.addCommand(createAuthCommand());
51
+ program.addCommand(createSearchCommand());
52
+ program.addCommand(createTrackCommand());
53
+ program.addCommand(createLibraryCommand());
54
+ program.addCommand(createPlaylistCommand());
55
+ program.addCommand(createPlayerCommand());
56
+ return program;
57
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createLibraryCommand(): Command;
@@ -0,0 +1,92 @@
1
+ import { Command } from 'commander';
2
+ import { getLikedTrackIds, likeTrack, getRecentTracks } from '../api/user.js';
3
+ import { getTrackDetails } from '../api/track.js';
4
+ import { output, outputError } from '../output/json.js';
5
+ import { ExitCode } from '../types/index.js';
6
+ export function createLibraryCommand() {
7
+ const library = new Command('library').description('User library');
8
+ library
9
+ .command('liked')
10
+ .description('Liked tracks')
11
+ .option('-l, --limit <number>', 'Limit', '50')
12
+ .action(async (options) => {
13
+ try {
14
+ const ids = await getLikedTrackIds();
15
+ const limit = parseInt(options.limit);
16
+ const limitedIds = ids.slice(0, limit);
17
+ if (limitedIds.length === 0) {
18
+ output({ tracks: [], total: 0 });
19
+ return;
20
+ }
21
+ const tracks = await getTrackDetails(limitedIds);
22
+ output({
23
+ tracks: tracks.map((t) => ({
24
+ id: t.id,
25
+ name: t.name,
26
+ artist: t.artists.map((a) => a.name).join(', '),
27
+ album: t.album.name,
28
+ uri: t.uri,
29
+ })),
30
+ total: ids.length,
31
+ showing: limitedIds.length,
32
+ });
33
+ }
34
+ catch (error) {
35
+ outputError('LIBRARY_ERROR', error instanceof Error ? error.message : 'Failed');
36
+ process.exit(ExitCode.NETWORK_ERROR);
37
+ }
38
+ });
39
+ library
40
+ .command('like')
41
+ .description('Like a track')
42
+ .argument('<track_id>', 'Track ID')
43
+ .action(async (trackId) => {
44
+ try {
45
+ await likeTrack(trackId, true);
46
+ output({ message: 'Liked', trackId });
47
+ }
48
+ catch (error) {
49
+ outputError('LIBRARY_ERROR', error instanceof Error ? error.message : 'Failed');
50
+ process.exit(ExitCode.NETWORK_ERROR);
51
+ }
52
+ });
53
+ library
54
+ .command('unlike')
55
+ .description('Unlike a track')
56
+ .argument('<track_id>', 'Track ID')
57
+ .action(async (trackId) => {
58
+ try {
59
+ await likeTrack(trackId, false);
60
+ output({ message: 'Unliked', trackId });
61
+ }
62
+ catch (error) {
63
+ outputError('LIBRARY_ERROR', error instanceof Error ? error.message : 'Failed');
64
+ process.exit(ExitCode.NETWORK_ERROR);
65
+ }
66
+ });
67
+ library
68
+ .command('recent')
69
+ .description('Recently played')
70
+ .option('-l, --limit <number>', 'Limit', '50')
71
+ .action(async (options) => {
72
+ try {
73
+ const limit = parseInt(options.limit);
74
+ const tracks = await getRecentTracks(limit);
75
+ output({
76
+ tracks: tracks.map((t) => ({
77
+ id: t.id,
78
+ name: t.name,
79
+ artist: t.artists.map((a) => a.name).join(', '),
80
+ album: t.album.name,
81
+ uri: t.uri,
82
+ })),
83
+ total: tracks.length,
84
+ });
85
+ }
86
+ catch (error) {
87
+ outputError('LIBRARY_ERROR', error instanceof Error ? error.message : 'Failed');
88
+ process.exit(ExitCode.NETWORK_ERROR);
89
+ }
90
+ });
91
+ return library;
92
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createPlayerCommand(): Command;