@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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/album.d.ts +2 -0
- package/dist/album.js +124 -0
- package/dist/artist.d.ts +5 -0
- package/dist/artist.js +230 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +226 -0
- package/dist/history.d.ts +3 -0
- package/dist/history.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +372 -0
- package/dist/library.d.ts +7 -0
- package/dist/library.js +131 -0
- package/dist/playback.d.ts +3 -0
- package/dist/playback.js +284 -0
- package/dist/playlist.d.ts +9 -0
- package/dist/playlist.js +269 -0
- package/dist/recommend.d.ts +1 -0
- package/dist/recommend.js +36 -0
- package/dist/search.d.ts +4 -0
- package/dist/search.js +183 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +120 -0
- package/dist/track.d.ts +4 -0
- package/dist/track.js +235 -0
- package/dist/user.d.ts +1 -0
- package/dist/user.js +36 -0
- package/package.json +59 -0
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> · <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
|
package/dist/history.js
ADDED
|
@@ -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
|
package/dist/index.d.ts
ADDED
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 {};
|