@lucaperret/tidal-cli 1.1.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/dist/auth.js ADDED
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ensureInit = ensureInit;
37
+ exports.authenticate = authenticate;
38
+ exports.getApiClient = getApiClient;
39
+ exports.getCountryCode = getCountryCode;
40
+ exports.doLogout = doLogout;
41
+ const session_1 = require("./session");
42
+ // Must install localStorage polyfill before importing auth
43
+ (0, session_1.installLocalStorage)();
44
+ const auth_1 = require("@tidal-music/auth");
45
+ const api_1 = require("@tidal-music/api");
46
+ const http = __importStar(require("http"));
47
+ const child_process_1 = require("child_process");
48
+ // Public client ID for the "Tidal CLI" application.
49
+ // This identifies the app, not a secret — standard OAuth public client pattern.
50
+ // Auth uses PKCE (code_challenge + code_verifier) instead of a client_secret.
51
+ const CLIENT_ID = 'PYVtmSHMTGI9oBUs';
52
+ const CREDENTIALS_STORAGE_KEY = 'tidal-cli';
53
+ const REDIRECT_PORT = 17893;
54
+ const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
55
+ const SCOPES = [
56
+ 'collection.read',
57
+ 'collection.write',
58
+ 'playlists.read',
59
+ 'playlists.write',
60
+ 'playback',
61
+ 'user.read',
62
+ 'recommendations.read',
63
+ 'entitlements.read',
64
+ 'search.read',
65
+ 'search.write',
66
+ ];
67
+ let initialized = false;
68
+ async function ensureInit() {
69
+ if (initialized)
70
+ return;
71
+ await (0, auth_1.init)({
72
+ clientId: CLIENT_ID,
73
+ credentialsStorageKey: CREDENTIALS_STORAGE_KEY,
74
+ scopes: SCOPES,
75
+ });
76
+ initialized = true;
77
+ }
78
+ function openBrowser(url) {
79
+ const cmd = process.platform === 'darwin' ? 'open'
80
+ : process.platform === 'win32' ? 'start'
81
+ : 'xdg-open';
82
+ (0, child_process_1.exec)(`${cmd} "${url}"`);
83
+ }
84
+ async function authenticate() {
85
+ await ensureInit();
86
+ const loginUrl = await (0, auth_1.initializeLogin)({ redirectUri: REDIRECT_URI });
87
+ // Wait for the OAuth callback on a local HTTP server
88
+ const code = await new Promise((resolve, reject) => {
89
+ const server = http.createServer((req, res) => {
90
+ const url = new URL(req.url ?? '/', `http://localhost:${REDIRECT_PORT}`);
91
+ if (url.pathname !== '/callback') {
92
+ res.writeHead(404);
93
+ res.end();
94
+ return;
95
+ }
96
+ const error = url.searchParams.get('error');
97
+ if (error) {
98
+ res.writeHead(200, { 'Content-Type': 'text/html' });
99
+ res.end('<h1>Authorization failed</h1><p>You can close this tab.</p>');
100
+ server.close();
101
+ reject(new Error(`OAuth error: ${error} — ${url.searchParams.get('error_description') ?? ''}`));
102
+ return;
103
+ }
104
+ const queryString = url.search.substring(1); // strip leading '?'
105
+ res.writeHead(200, { 'Content-Type': 'text/html' });
106
+ res.end(`<!DOCTYPE html>
107
+ <html><head><meta charset="utf-8"><title>tidal-cli — Authorized</title>
108
+ <style>
109
+ * { margin: 0; padding: 0; box-sizing: border-box; }
110
+ body { font-family: system-ui, -apple-system, sans-serif; background: #000; color: #fff; min-height: 100vh; display: flex; justify-content: center; padding: 60px 20px; }
111
+ .container { max-width: 600px; width: 100%; }
112
+
113
+ .hero { text-align: center; margin-bottom: 48px; }
114
+ .logo { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 24px; }
115
+ .logo svg { color: #00ffff; }
116
+ .logo span { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; }
117
+ h1 { font-size: 36px; font-weight: 700; margin-bottom: 4px; }
118
+ h1 em { font-style: normal; color: #00ffff; }
119
+ .status { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px; }
120
+ .status .dot { width: 6px; height: 6px; border-radius: 50%; background: #00ffff; }
121
+ .status span { font-size: 13px; color: #00ffff; font-family: monospace; }
122
+ .subtitle { color: #888; font-size: 15px; line-height: 1.5; }
123
+
124
+ .section-label { font-size: 11px; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: 1.5px; margin: 32px 0 12px; }
125
+
126
+ .cmd { background: #111; border: 1px solid #1a1a1a; border-radius: 10px; padding: 14px 16px; margin-bottom: 6px; transition: border-color 0.2s; }
127
+ .cmd:hover { border-color: rgba(0,255,255,0.2); }
128
+ .cmd code { color: #fff; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
129
+ .cmd code .prompt { color: rgba(0,255,255,0.5); user-select: none; }
130
+ .cmd .desc { color: #666; font-size: 12px; margin-top: 4px; }
131
+
132
+ .prompts { margin-top: 40px; padding-top: 32px; border-top: 1px solid #1a1a1a; }
133
+ .prompts h2 { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
134
+ .prompts .sub { color: #888; font-size: 14px; margin-bottom: 16px; }
135
+ .prompt-card { background: #111; border: 1px solid #1a1a1a; border-radius: 10px; padding: 14px 16px; margin-bottom: 6px; cursor: default; transition: border-color 0.2s; }
136
+ .prompt-card:hover { border-color: rgba(0,255,255,0.2); }
137
+ .prompt-card .label { font-size: 13px; color: #fff; margin-bottom: 2px; }
138
+ .prompt-card .example { font-size: 12px; color: #555; font-style: italic; }
139
+
140
+ .footer { margin-top: 40px; text-align: center; color: #333; font-size: 12px; }
141
+ .footer a { color: #00ffff; text-decoration: none; }
142
+ .footer a:hover { text-decoration: underline; }
143
+ </style></head><body><div class="container">
144
+
145
+ <div class="hero">
146
+ <div class="logo">
147
+ <svg width="32" height="32" viewBox="0 0 40 40" fill="none"><path d="M20 4L28 12L20 20L12 12L20 4Z" fill="currentColor"/><path d="M12 12L20 20L12 28L4 20L12 12Z" fill="currentColor" opacity="0.7"/><path d="M28 12L36 20L28 28L20 20L28 12Z" fill="currentColor" opacity="0.7"/><path d="M20 20L28 28L20 36L12 28L20 20Z" fill="currentColor" opacity="0.4"/></svg>
148
+ <span>tidal-cli</span>
149
+ </div>
150
+ <h1>You're <em>in</em></h1>
151
+ <div class="status"><div class="dot"></div><span>authenticated</span></div>
152
+ <p class="subtitle">Your AI agent can now control Tidal. Try one of these prompts.</p>
153
+ </div>
154
+
155
+ <div class="section-label">Ask your AI agent</div>
156
+ <div class="prompt-card"><div class="label">Create a playlist with the best tracks from Daft Punk's Discovery album</div><div class="example">Searches, creates playlist, adds tracks</div></div>
157
+ <div class="prompt-card"><div class="label">Find artists similar to Massive Attack and add their top tracks to my library</div><div class="example">Searches catalog, adds to favorites</div></div>
158
+ <div class="prompt-card"><div class="label">What are my playlists? Add the new LCD Soundsystem album to the first one</div><div class="example">Lists playlists, searches album, adds tracks</div></div>
159
+ <div class="prompt-card"><div class="label">Play me something by Boards of Canada</div><div class="example">Searches, picks a track, plays it</div></div>
160
+ <div class="prompt-card"><div class="label">Build a 2000s indie rock playlist with The Strokes, Arctic Monkeys, and Interpol</div><div class="example">Multi-step: create, search, add tracks</div></div>
161
+
162
+ <div class="section-label">Or use the CLI directly</div>
163
+ <div class="cmd"><code><span class="prompt">$ </span>tidal-cli search track "Around the World"</code><div class="desc">Search for tracks, artists, or albums</div></div>
164
+ <div class="cmd"><code><span class="prompt">$ </span>tidal-cli playlist list</code><div class="desc">List your playlists</div></div>
165
+ <div class="cmd"><code><span class="prompt">$ </span>tidal-cli playlist create --name "Chill Vibes"</code><div class="desc">Create a new playlist</div></div>
166
+ <div class="cmd"><code><span class="prompt">$ </span>tidal-cli playback play 21844140</code><div class="desc">Play a track locally</div></div>
167
+
168
+ <div class="footer">
169
+ <a href="https://clawhub.ai/lucaperret/tidal-cli">ClawHub</a> &middot; <a href="https://github.com/lucaperret/tidal-cli">GitHub</a>
170
+ </div>
171
+
172
+ </div></body></html>`);
173
+ server.close();
174
+ resolve(queryString);
175
+ });
176
+ server.listen(REDIRECT_PORT, () => {
177
+ console.log('\nOpening browser for Tidal authorization...');
178
+ console.log(`If the browser doesn't open, visit:\n ${loginUrl}\n`);
179
+ console.log('Waiting for authorization...');
180
+ openBrowser(loginUrl);
181
+ });
182
+ server.on('error', (err) => {
183
+ reject(new Error(`Failed to start callback server on port ${REDIRECT_PORT}: ${err.message}`));
184
+ });
185
+ });
186
+ await (0, auth_1.finalizeLogin)(code);
187
+ const creds = await auth_1.credentialsProvider.getCredentials();
188
+ console.log(`\nAuthenticated successfully! User ID: ${creds.userId ?? 'unknown'}`);
189
+ }
190
+ async function getApiClient() {
191
+ await ensureInit();
192
+ // Verify we have valid credentials
193
+ const creds = await auth_1.credentialsProvider.getCredentials();
194
+ if (!creds.token) {
195
+ console.error('Error: Not authenticated. Run `tidal-cli auth` first.');
196
+ process.exit(1);
197
+ }
198
+ return (0, api_1.createAPIClient)(auth_1.credentialsProvider);
199
+ }
200
+ let cachedCountryCode = null;
201
+ async function getCountryCode() {
202
+ if (cachedCountryCode)
203
+ return cachedCountryCode;
204
+ try {
205
+ const client = await getApiClient();
206
+ const { data } = await client.GET('/users/{id}', {
207
+ params: { path: { id: 'me' } },
208
+ });
209
+ const country = data?.data?.attributes?.country;
210
+ if (country) {
211
+ cachedCountryCode = country;
212
+ return country;
213
+ }
214
+ }
215
+ catch {
216
+ // fall through to default
217
+ }
218
+ cachedCountryCode = process.env.TIDAL_COUNTRY ?? 'US';
219
+ return cachedCountryCode;
220
+ }
221
+ async function doLogout() {
222
+ await ensureInit();
223
+ (0, auth_1.logout)();
224
+ console.log('Logged out successfully.');
225
+ }
226
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,3 @@
1
+ type RecentType = 'tracks' | 'albums' | 'artists';
2
+ export declare function getRecentlyAdded(type: RecentType, json: boolean): Promise<void>;
3
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRecentlyAdded = getRecentlyAdded;
4
+ const auth_1 = require("./auth");
5
+ const endpointMap = {
6
+ tracks: '/userCollectionTracks/{id}/relationships/items',
7
+ albums: '/userCollectionAlbums/{id}/relationships/items',
8
+ artists: '/userCollectionArtists/{id}/relationships/items',
9
+ };
10
+ const includeTypeMap = {
11
+ tracks: 'tracks',
12
+ albums: 'albums',
13
+ artists: 'artists',
14
+ };
15
+ async function getRecentlyAdded(type, json) {
16
+ const client = await (0, auth_1.getApiClient)();
17
+ const countryCode = await (0, auth_1.getCountryCode)();
18
+ const { data, error } = await client.GET(endpointMap[type], {
19
+ params: {
20
+ path: { id: 'me' },
21
+ query: {
22
+ countryCode,
23
+ include: ['items'],
24
+ sort: ['-addedAt'],
25
+ },
26
+ },
27
+ });
28
+ if (error || !data) {
29
+ console.error(`Error: Failed to get recently added ${type} — ${JSON.stringify(error)}`);
30
+ process.exit(1);
31
+ }
32
+ const included = data.included ?? [];
33
+ const items = included
34
+ .filter((item) => item.type === includeTypeMap[type])
35
+ .map((item) => {
36
+ const attrs = item.attributes;
37
+ return {
38
+ id: item.id,
39
+ name: attrs?.title ?? attrs?.name ?? 'Unknown',
40
+ addedAt: attrs?.addedAt,
41
+ };
42
+ });
43
+ // Enrich with addedAt from the relationship data if available
44
+ const relData = data.data ?? [];
45
+ for (const rel of relData) {
46
+ const addedAt = rel.meta?.addedAt;
47
+ if (addedAt) {
48
+ const match = items.find((i) => i.id === rel.id);
49
+ if (match && !match.addedAt) {
50
+ match.addedAt = addedAt;
51
+ }
52
+ }
53
+ }
54
+ if (json) {
55
+ console.log(JSON.stringify(items, null, 2));
56
+ return;
57
+ }
58
+ if (items.length === 0) {
59
+ console.log(`No recently added ${type} found.`);
60
+ return;
61
+ }
62
+ console.log(`\nRecently added ${type}:\n`);
63
+ for (const item of items) {
64
+ const date = item.addedAt ? ` (added: ${item.addedAt})` : '';
65
+ console.log(` [${item.id}] ${item.name}${date}`);
66
+ }
67
+ console.log();
68
+ }
69
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // Suppress "TrueTime is not yet synchronized" warnings from @tidal-music/auth
5
+ const originalWarn = console.warn;
6
+ console.warn = (...args) => {
7
+ if (typeof args[0] === 'string' && args[0].includes('TrueTime'))
8
+ return;
9
+ originalWarn(...args);
10
+ };
11
+ const commander_1 = require("commander");
12
+ const auth_1 = require("./auth");
13
+ const search_1 = require("./search");
14
+ const playlist_1 = require("./playlist");
15
+ const library_1 = require("./library");
16
+ const playback_1 = require("./playback");
17
+ const artist_1 = require("./artist");
18
+ const track_1 = require("./track");
19
+ const album_1 = require("./album");
20
+ const recommend_1 = require("./recommend");
21
+ const user_1 = require("./user");
22
+ const history_1 = require("./history");
23
+ const program = new commander_1.Command();
24
+ program
25
+ .name('tidal-cli')
26
+ .description('CLI for Tidal music streaming service')
27
+ .version('1.0.0')
28
+ .option('--json', 'Output as JSON');
29
+ // Auth
30
+ program
31
+ .command('auth')
32
+ .description('Authenticate with Tidal (OAuth Authorization Code Flow)')
33
+ .action(wrapAction(async () => {
34
+ await (0, auth_1.authenticate)();
35
+ }));
36
+ program
37
+ .command('logout')
38
+ .description('Clear stored credentials')
39
+ .action(wrapAction(async () => {
40
+ await (0, auth_1.doLogout)();
41
+ }));
42
+ // Search
43
+ const searchCmd = program
44
+ .command('search')
45
+ .description('Search Tidal catalog');
46
+ searchCmd
47
+ .command('artist <query>')
48
+ .description('Search for artists')
49
+ .action(wrapAction(async (query) => {
50
+ await (0, search_1.search)('artist', query, getJson());
51
+ }));
52
+ searchCmd
53
+ .command('album <query>')
54
+ .description('Search for albums')
55
+ .action(wrapAction(async (query) => {
56
+ await (0, search_1.search)('album', query, getJson());
57
+ }));
58
+ searchCmd
59
+ .command('track <query>')
60
+ .description('Search for tracks')
61
+ .action(wrapAction(async (query) => {
62
+ await (0, search_1.search)('track', query, getJson());
63
+ }));
64
+ searchCmd
65
+ .command('video <query>')
66
+ .description('Search for videos')
67
+ .action(wrapAction(async (query) => {
68
+ await (0, search_1.search)('video', query, getJson());
69
+ }));
70
+ searchCmd
71
+ .command('playlist <query>')
72
+ .description('Search for playlists')
73
+ .action(wrapAction(async (query) => {
74
+ await (0, search_1.search)('playlist', query, getJson());
75
+ }));
76
+ searchCmd
77
+ .command('suggest <query>')
78
+ .description('Get search suggestions')
79
+ .action(wrapAction(async (query) => {
80
+ await (0, search_1.searchSuggestions)(query, getJson());
81
+ }));
82
+ searchCmd
83
+ .command('editorial [genre]')
84
+ .description('Browse editorial playlists by genre or keyword')
85
+ .action(wrapAction(async (genre) => {
86
+ await (0, search_1.search)('playlist', genre || 'top hits', getJson());
87
+ }));
88
+ // Artist
89
+ const artistCmd = program
90
+ .command('artist')
91
+ .description('Get artist information');
92
+ artistCmd
93
+ .command('info <id>')
94
+ .description('Get artist info')
95
+ .action(wrapAction(async (id) => {
96
+ await (0, artist_1.getArtistInfo)(id, getJson());
97
+ }));
98
+ artistCmd
99
+ .command('tracks <id>')
100
+ .description('Get top tracks for an artist')
101
+ .action(wrapAction(async (id) => {
102
+ await (0, artist_1.getArtistTracks)(id, getJson());
103
+ }));
104
+ artistCmd
105
+ .command('albums <id>')
106
+ .description('Get albums for an artist')
107
+ .action(wrapAction(async (id) => {
108
+ await (0, artist_1.getArtistAlbums)(id, getJson());
109
+ }));
110
+ artistCmd
111
+ .command('similar <id>')
112
+ .description('Get artists similar to a given artist')
113
+ .action(wrapAction(async (id) => {
114
+ await (0, artist_1.getSimilarArtists)(id, getJson());
115
+ }));
116
+ artistCmd
117
+ .command('radio <id>')
118
+ .description('Get radio tracks for an artist')
119
+ .action(wrapAction(async (id) => {
120
+ await (0, artist_1.getArtistRadio)(id, getJson());
121
+ }));
122
+ // Track
123
+ const trackCmd = program
124
+ .command('track')
125
+ .description('Get track information');
126
+ trackCmd
127
+ .command('info <id>')
128
+ .description('Get track info')
129
+ .action(wrapAction(async (id) => {
130
+ await (0, track_1.getTrackInfo)(id, getJson());
131
+ }));
132
+ trackCmd
133
+ .command('similar <id>')
134
+ .description('Get tracks similar to a given track')
135
+ .action(wrapAction(async (id) => {
136
+ await (0, track_1.getSimilarTracks)(id, getJson());
137
+ }));
138
+ trackCmd
139
+ .command('radio <id>')
140
+ .description('Get radio tracks for a track')
141
+ .action(wrapAction(async (id) => {
142
+ await (0, track_1.getTrackRadio)(id, getJson());
143
+ }));
144
+ trackCmd
145
+ .command('isrc <isrc>')
146
+ .description('Find tracks by ISRC code')
147
+ .action(wrapAction(async (isrc) => {
148
+ await (0, track_1.getTrackByIsrc)(isrc, getJson());
149
+ }));
150
+ // Album
151
+ const albumCmd = program
152
+ .command('album')
153
+ .description('Get album information');
154
+ albumCmd
155
+ .command('info <id>')
156
+ .description('Get album info')
157
+ .action(wrapAction(async (id) => {
158
+ await (0, album_1.getAlbumInfo)(id, getJson());
159
+ }));
160
+ albumCmd
161
+ .command('barcode <barcode>')
162
+ .description('Find albums by barcode')
163
+ .action(wrapAction(async (barcode) => {
164
+ await (0, album_1.getAlbumByBarcode)(barcode, getJson());
165
+ }));
166
+ // Recommend
167
+ program
168
+ .command('recommend')
169
+ .description('Get personalized recommendations')
170
+ .action(wrapAction(async () => {
171
+ await (0, recommend_1.getRecommendations)(getJson());
172
+ }));
173
+ // User
174
+ const userCmd = program
175
+ .command('user')
176
+ .description('User account commands');
177
+ userCmd
178
+ .command('profile')
179
+ .description('Get your user profile')
180
+ .action(wrapAction(async () => {
181
+ await (0, user_1.getUserProfile)(getJson());
182
+ }));
183
+ // Playlist
184
+ const playlistCmd = program
185
+ .command('playlist')
186
+ .description('Manage playlists');
187
+ playlistCmd
188
+ .command('list')
189
+ .description('List your playlists')
190
+ .action(wrapAction(async () => {
191
+ await (0, playlist_1.listPlaylists)(getJson());
192
+ }));
193
+ playlistCmd
194
+ .command('create')
195
+ .description('Create a new playlist')
196
+ .requiredOption('--name <name>', 'Playlist name')
197
+ .option('--desc <description>', 'Playlist description', '')
198
+ .action(wrapAction(async (opts) => {
199
+ await (0, playlist_1.createPlaylist)(opts.name, opts.desc, getJson());
200
+ }));
201
+ playlistCmd
202
+ .command('rename')
203
+ .description('Rename a playlist')
204
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
205
+ .requiredOption('--name <name>', 'New name')
206
+ .action(wrapAction(async (opts) => {
207
+ await (0, playlist_1.renamePlaylist)(opts.playlistId, opts.name, getJson());
208
+ }));
209
+ playlistCmd
210
+ .command('delete')
211
+ .description('Delete a playlist')
212
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
213
+ .action(wrapAction(async (opts) => {
214
+ await (0, playlist_1.deletePlaylist)(opts.playlistId, getJson());
215
+ }));
216
+ playlistCmd
217
+ .command('add-track')
218
+ .description('Add a track to a playlist')
219
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
220
+ .requiredOption('--track-id <id>', 'Track ID')
221
+ .action(wrapAction(async (opts) => {
222
+ await (0, playlist_1.addTrackToPlaylist)(opts.playlistId, opts.trackId, getJson());
223
+ }));
224
+ playlistCmd
225
+ .command('remove-track')
226
+ .description('Remove a track from a playlist')
227
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
228
+ .requiredOption('--track-id <id>', 'Track ID')
229
+ .action(wrapAction(async (opts) => {
230
+ await (0, playlist_1.removeTrackFromPlaylist)(opts.playlistId, opts.trackId, getJson());
231
+ }));
232
+ playlistCmd
233
+ .command('add-album')
234
+ .description('Add all tracks from an album to a playlist')
235
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
236
+ .requiredOption('--album-id <id>', 'Album ID')
237
+ .action(wrapAction(async (opts) => {
238
+ await (0, playlist_1.addAlbumToPlaylist)(opts.playlistId, opts.albumId, getJson());
239
+ }));
240
+ playlistCmd
241
+ .command('move-track')
242
+ .description('Move a track within a playlist')
243
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
244
+ .requiredOption('--track-id <id>', 'Track ID to move')
245
+ .requiredOption('--before <itemId>', 'Item ID to place before (use "end" for last position)')
246
+ .action(wrapAction(async (opts) => {
247
+ await (0, playlist_1.moveTrackInPlaylist)(opts.playlistId, opts.trackId, opts.before, getJson());
248
+ }));
249
+ playlistCmd
250
+ .command('set-description')
251
+ .description('Update playlist description')
252
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
253
+ .requiredOption('--desc <description>', 'New description')
254
+ .action(wrapAction(async (opts) => {
255
+ await (0, playlist_1.updatePlaylistDescription)(opts.playlistId, opts.desc, getJson());
256
+ }));
257
+ // Library
258
+ const libraryCmd = program
259
+ .command('library')
260
+ .description('Manage your library/favorites');
261
+ libraryCmd
262
+ .command('add')
263
+ .description('Add an item to your library')
264
+ .option('--artist-id <id>', 'Artist ID')
265
+ .option('--album-id <id>', 'Album ID')
266
+ .option('--track-id <id>', 'Track ID')
267
+ .option('--video-id <id>', 'Video ID')
268
+ .action(wrapAction(async (opts) => {
269
+ const { type, id } = resolveLibraryArgs(opts);
270
+ await (0, library_1.addToLibrary)(type, id, getJson());
271
+ }));
272
+ libraryCmd
273
+ .command('remove')
274
+ .description('Remove an item from your library')
275
+ .option('--artist-id <id>', 'Artist ID')
276
+ .option('--album-id <id>', 'Album ID')
277
+ .option('--track-id <id>', 'Track ID')
278
+ .option('--video-id <id>', 'Video ID')
279
+ .action(wrapAction(async (opts) => {
280
+ const { type, id } = resolveLibraryArgs(opts);
281
+ await (0, library_1.removeFromLibrary)(type, id, getJson());
282
+ }));
283
+ libraryCmd
284
+ .command('favorite-playlists')
285
+ .description('List your favorited playlists')
286
+ .action(wrapAction(async () => {
287
+ await (0, library_1.listFavoritedPlaylists)(getJson());
288
+ }));
289
+ libraryCmd
290
+ .command('add-playlist')
291
+ .description('Add a playlist to your favorites')
292
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
293
+ .action(wrapAction(async (opts) => {
294
+ await (0, library_1.addPlaylistToFavorites)(opts.playlistId, getJson());
295
+ }));
296
+ libraryCmd
297
+ .command('remove-playlist')
298
+ .description('Remove a playlist from your favorites')
299
+ .requiredOption('--playlist-id <id>', 'Playlist ID')
300
+ .action(wrapAction(async (opts) => {
301
+ await (0, library_1.removePlaylistFromFavorites)(opts.playlistId, getJson());
302
+ }));
303
+ // History
304
+ const historyCmd = program.command('history').description('Recently added to library');
305
+ historyCmd
306
+ .command('tracks')
307
+ .description('Recently added tracks')
308
+ .action(wrapAction(async () => {
309
+ await (0, history_1.getRecentlyAdded)('tracks', getJson());
310
+ }));
311
+ historyCmd
312
+ .command('albums')
313
+ .description('Recently added albums')
314
+ .action(wrapAction(async () => {
315
+ await (0, history_1.getRecentlyAdded)('albums', getJson());
316
+ }));
317
+ historyCmd
318
+ .command('artists')
319
+ .description('Recently added artists')
320
+ .action(wrapAction(async () => {
321
+ await (0, history_1.getRecentlyAdded)('artists', getJson());
322
+ }));
323
+ // Playback
324
+ const playbackCmd = program
325
+ .command('playback')
326
+ .description('Track playback and streaming');
327
+ playbackCmd
328
+ .command('info <track-id>')
329
+ .description('Get playback info for a track')
330
+ .option('--quality <quality>', 'Audio quality (LOW, HIGH, LOSSLESS, HI_RES)', 'HIGH')
331
+ .action(wrapAction(async (trackId, opts) => {
332
+ await (0, playback_1.playbackInfo)(trackId, opts.quality, getJson());
333
+ }));
334
+ playbackCmd
335
+ .command('url <track-id>')
336
+ .description('Get direct stream URL for a track')
337
+ .option('--quality <quality>', 'Audio quality (LOW, HIGH, LOSSLESS, HI_RES)', 'HIGH')
338
+ .action(wrapAction(async (trackId, opts) => {
339
+ await (0, playback_1.playbackUrl)(trackId, opts.quality, getJson());
340
+ }));
341
+ playbackCmd
342
+ .command('play <track-id>')
343
+ .description('Play a track locally')
344
+ .option('--quality <quality>', 'Audio quality (LOW, HIGH, LOSSLESS, HI_RES)', 'HIGH')
345
+ .action(wrapAction(async (trackId, opts) => {
346
+ await (0, playback_1.playbackPlay)(trackId, opts.quality);
347
+ }));
348
+ function resolveLibraryArgs(opts) {
349
+ if (opts.artistId)
350
+ return { type: 'artist', id: opts.artistId };
351
+ if (opts.albumId)
352
+ return { type: 'album', id: opts.albumId };
353
+ if (opts.trackId)
354
+ return { type: 'track', id: opts.trackId };
355
+ if (opts.videoId)
356
+ return { type: 'video', id: opts.videoId };
357
+ console.error('Error: Must specify one of --artist-id, --album-id, --track-id, or --video-id');
358
+ process.exit(2);
359
+ }
360
+ function getJson() {
361
+ return program.opts().json ?? false;
362
+ }
363
+ function wrapAction(fn) {
364
+ return (...args) => {
365
+ fn(...args).catch((err) => {
366
+ console.error(`Error: ${err.message}`);
367
+ process.exit(1);
368
+ });
369
+ };
370
+ }
371
+ program.parse();
372
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ type LibraryResourceType = 'artist' | 'album' | 'track' | 'video';
2
+ export declare function addToLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
3
+ export declare function removeFromLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
4
+ export declare function listFavoritedPlaylists(json: boolean): Promise<void>;
5
+ export declare function addPlaylistToFavorites(playlistId: string, json: boolean): Promise<void>;
6
+ export declare function removePlaylistFromFavorites(playlistId: string, json: boolean): Promise<void>;
7
+ export {};