@bzenky/spoti 0.4.0 → 0.5.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/README.md +19 -7
- package/dist/app.d.ts +6 -2
- package/dist/app.js +109 -51
- package/dist/app.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/services/device.service.d.ts +3 -3
- package/dist/services/device.service.js +19 -11
- package/dist/services/device.service.js.map +1 -1
- package/dist/services/player.service.d.ts +4 -2
- package/dist/services/player.service.js +35 -5
- package/dist/services/player.service.js.map +1 -1
- package/dist/services/queue.service.js +11 -6
- package/dist/services/queue.service.js.map +1 -1
- package/dist/services/search.service.d.ts +4 -4
- package/dist/services/search.service.js +10 -9
- package/dist/services/search.service.js.map +1 -1
- package/dist/spotify/client.d.ts +2 -0
- package/dist/spotify/client.js +170 -46
- package/dist/spotify/client.js.map +1 -1
- package/dist/spotify/types.d.ts +2 -0
- package/dist/ui/completions.js +10 -10
- package/dist/ui/interactive-search.d.ts +4 -1
- package/dist/ui/interactive-search.js +218 -55
- package/dist/ui/interactive-search.js.map +1 -1
- package/dist/ui/output.d.ts +27 -11
- package/dist/ui/output.js +87 -45
- package/dist/ui/output.js.map +1 -1
- package/dist/ui/prompts.d.ts +11 -1
- package/dist/ui/prompts.js +65 -13
- package/dist/ui/prompts.js.map +1 -1
- package/dist/ui/watch.js +3 -2
- package/dist/ui/watch.js.map +1 -1
- package/dist/utils/errors.js +2 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/text.d.ts +2 -0
- package/dist/utils/text.js +10 -0
- package/dist/utils/text.js.map +1 -0
- package/package.json +2 -1
package/dist/spotify/client.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { AppError, AuthenticationRequiredError, NoActiveDeviceError, PremiumRequiredError, RateLimitedError, SpotifyApiError, toError, } from '../utils/errors.js';
|
|
2
|
+
import { sanitizeOneLineText } from '../utils/text.js';
|
|
2
3
|
const API_BASE_URL = 'https://api.spotify.com/v1';
|
|
3
4
|
const MAX_RATE_LIMIT_RETRIES = 3;
|
|
5
|
+
const PLAYBACK_CONTROL_REQUESTS = new Set([
|
|
6
|
+
'PUT /me/player',
|
|
7
|
+
'PUT /me/player/play',
|
|
8
|
+
'PUT /me/player/pause',
|
|
9
|
+
'POST /me/player/next',
|
|
10
|
+
'POST /me/player/previous',
|
|
11
|
+
'PUT /me/player/seek',
|
|
12
|
+
'PUT /me/player/repeat',
|
|
13
|
+
'PUT /me/player/volume',
|
|
14
|
+
'PUT /me/player/shuffle',
|
|
15
|
+
'POST /me/player/queue',
|
|
16
|
+
]);
|
|
4
17
|
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
5
18
|
export class SpotifyClient {
|
|
6
19
|
authService;
|
|
@@ -35,83 +48,194 @@ export class SpotifyClient {
|
|
|
35
48
|
headers.set('Content-Type', 'application/json');
|
|
36
49
|
body = JSON.stringify(options.body);
|
|
37
50
|
}
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
throwIfExternallyAborted(options.signal);
|
|
52
|
+
let accessToken = await this.authService.getAccessToken();
|
|
53
|
+
let refreshedToken = false;
|
|
54
|
+
let rateLimitRetries = 0;
|
|
55
|
+
while (true) {
|
|
56
|
+
throwIfExternallyAborted(options.signal);
|
|
57
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
58
|
+
const attempt = createAttemptSignal(options.signal);
|
|
59
|
+
let shouldRefreshToken = false;
|
|
60
|
+
let retryDelayMs;
|
|
40
61
|
try {
|
|
41
|
-
|
|
62
|
+
const response = await this.fetcher(url, {
|
|
42
63
|
method,
|
|
43
64
|
headers,
|
|
44
|
-
signal:
|
|
65
|
+
signal: attempt.signal,
|
|
45
66
|
...(body === undefined ? {} : { body }),
|
|
46
67
|
});
|
|
68
|
+
if (response.status === 401 && !refreshedToken) {
|
|
69
|
+
shouldRefreshToken = true;
|
|
70
|
+
}
|
|
71
|
+
else if (response.status === 429 &&
|
|
72
|
+
rateLimitRetries < MAX_RATE_LIMIT_RETRIES) {
|
|
73
|
+
const retryAfterSeconds = getRetryAfterSeconds(response);
|
|
74
|
+
const exponentialBackoffMs = 500 * 2 ** rateLimitRetries;
|
|
75
|
+
rateLimitRetries += 1;
|
|
76
|
+
retryDelayMs = Math.max(retryAfterSeconds * 1_000, exponentialBackoffMs);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const mappedError = await mapSpotifyError(response, method, path);
|
|
81
|
+
throwIfAttemptAborted(attempt.signal, options.signal);
|
|
82
|
+
throw mappedError;
|
|
83
|
+
}
|
|
84
|
+
if (response.status === 204)
|
|
85
|
+
return undefined;
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
throwIfAttemptAborted(attempt.signal, options.signal);
|
|
88
|
+
if (!text)
|
|
89
|
+
return undefined;
|
|
90
|
+
const contentType = response.headers.get('content-type')?.toLocaleLowerCase() ?? '';
|
|
91
|
+
if (!contentType.includes('json')) {
|
|
92
|
+
if (method !== 'GET')
|
|
93
|
+
return undefined;
|
|
94
|
+
throw new SpotifyApiError('Spotify returned an unexpected non-JSON response.', response.status);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new SpotifyApiError('Spotify returned malformed JSON.', response.status);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
47
103
|
}
|
|
48
104
|
catch (error) {
|
|
49
|
-
|
|
50
|
-
if (normalizedError.name === 'TimeoutError') {
|
|
51
|
-
throw new AppError('Spotify did not respond within 15 seconds. Try again.');
|
|
52
|
-
}
|
|
53
|
-
throw new AppError(`Unable to reach Spotify: ${normalizedError.message}`);
|
|
105
|
+
throw normalizeAttemptError(error, attempt.signal, options.signal);
|
|
54
106
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
while (true) {
|
|
61
|
-
response = await execute(accessToken);
|
|
62
|
-
if (response.status === 401 && !refreshedToken) {
|
|
107
|
+
finally {
|
|
108
|
+
attempt.cleanup();
|
|
109
|
+
}
|
|
110
|
+
if (shouldRefreshToken) {
|
|
111
|
+
throwIfExternallyAborted(options.signal);
|
|
63
112
|
accessToken = await this.authService.forceRefreshAccessToken();
|
|
64
113
|
refreshedToken = true;
|
|
65
114
|
continue;
|
|
66
115
|
}
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
const exponentialBackoffMs = 500 * 2 ** rateLimitRetries;
|
|
70
|
-
rateLimitRetries += 1;
|
|
71
|
-
await this.sleeper(Math.max(retryAfterMs, exponentialBackoffMs));
|
|
116
|
+
if (retryDelayMs !== undefined) {
|
|
117
|
+
await this.waitForRetry(retryDelayMs, options.signal);
|
|
72
118
|
continue;
|
|
73
119
|
}
|
|
74
|
-
break;
|
|
75
120
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return undefined;
|
|
87
|
-
throw new SpotifyApiError('Spotify returned an unexpected non-JSON response.', response.status);
|
|
121
|
+
}
|
|
122
|
+
async waitForRetry(milliseconds, signal) {
|
|
123
|
+
throwIfExternallyAborted(signal);
|
|
124
|
+
if (!signal) {
|
|
125
|
+
await this.sleeper(milliseconds);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (this.sleeper === sleep) {
|
|
129
|
+
await abortableSleep(milliseconds, signal);
|
|
130
|
+
return;
|
|
88
131
|
}
|
|
132
|
+
let abortListener;
|
|
133
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
134
|
+
abortListener = () => reject(getExternalAbortReason(signal));
|
|
135
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
136
|
+
if (signal.aborted)
|
|
137
|
+
abortListener();
|
|
138
|
+
});
|
|
89
139
|
try {
|
|
90
|
-
|
|
140
|
+
await Promise.race([this.sleeper(milliseconds), aborted]);
|
|
91
141
|
}
|
|
92
|
-
|
|
93
|
-
|
|
142
|
+
finally {
|
|
143
|
+
if (abortListener)
|
|
144
|
+
signal.removeEventListener('abort', abortListener);
|
|
94
145
|
}
|
|
95
146
|
}
|
|
96
147
|
}
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
148
|
+
function createAttemptSignal(externalSignal) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const abortFromExternalSignal = () => {
|
|
151
|
+
if (externalSignal)
|
|
152
|
+
controller.abort(getExternalAbortReason(externalSignal));
|
|
153
|
+
};
|
|
154
|
+
externalSignal?.addEventListener('abort', abortFromExternalSignal, { once: true });
|
|
155
|
+
if (externalSignal?.aborted)
|
|
156
|
+
abortFromExternalSignal();
|
|
157
|
+
const timeout = setTimeout(() => controller.abort(new DOMException('The operation timed out.', 'TimeoutError')), 15_000);
|
|
158
|
+
timeout.unref();
|
|
159
|
+
let cleanedUp = false;
|
|
160
|
+
return {
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
cleanup: () => {
|
|
163
|
+
if (cleanedUp)
|
|
164
|
+
return;
|
|
165
|
+
cleanedUp = true;
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
externalSignal?.removeEventListener('abort', abortFromExternalSignal);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function normalizeAttemptError(error, attemptSignal, externalSignal) {
|
|
172
|
+
if (externalSignal?.aborted)
|
|
173
|
+
return getExternalAbortReason(externalSignal);
|
|
174
|
+
const normalizedError = toError(error);
|
|
175
|
+
if (normalizedError.name === 'TimeoutError' ||
|
|
176
|
+
(attemptSignal.aborted && toError(attemptSignal.reason).name === 'TimeoutError')) {
|
|
177
|
+
return new AppError('Spotify did not respond within 15 seconds. Try again.');
|
|
178
|
+
}
|
|
179
|
+
if (normalizedError instanceof AppError)
|
|
180
|
+
return normalizedError;
|
|
181
|
+
return new AppError(`Unable to reach Spotify: ${sanitizeOneLineText(normalizedError.message)}`);
|
|
182
|
+
}
|
|
183
|
+
function throwIfAttemptAborted(attemptSignal, externalSignal) {
|
|
184
|
+
throwIfExternallyAborted(externalSignal);
|
|
185
|
+
if (attemptSignal.aborted)
|
|
186
|
+
throw attemptSignal.reason;
|
|
100
187
|
}
|
|
101
|
-
|
|
188
|
+
function throwIfExternallyAborted(signal) {
|
|
189
|
+
if (signal?.aborted)
|
|
190
|
+
throw getExternalAbortReason(signal);
|
|
191
|
+
}
|
|
192
|
+
function getExternalAbortReason(signal) {
|
|
193
|
+
const reason = signal.reason;
|
|
194
|
+
if (reason instanceof Error && reason.name === 'AbortError')
|
|
195
|
+
return reason;
|
|
196
|
+
return new DOMException('The operation was aborted.', 'AbortError');
|
|
197
|
+
}
|
|
198
|
+
function abortableSleep(milliseconds, signal) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
signal.removeEventListener('abort', abortListener);
|
|
202
|
+
resolve();
|
|
203
|
+
}, milliseconds);
|
|
204
|
+
const abortListener = () => {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
reject(getExternalAbortReason(signal));
|
|
207
|
+
};
|
|
208
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
209
|
+
if (signal.aborted)
|
|
210
|
+
abortListener();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function getRetryAfterSeconds(response) {
|
|
214
|
+
const header = response.headers.get('retry-after');
|
|
215
|
+
if (header === null)
|
|
216
|
+
return 1;
|
|
217
|
+
const seconds = Number(header);
|
|
218
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
219
|
+
return 1;
|
|
220
|
+
return Math.ceil(seconds);
|
|
221
|
+
}
|
|
222
|
+
async function mapSpotifyError(response, method, path) {
|
|
102
223
|
const body = await readErrorBody(response);
|
|
103
224
|
const spotifyMessage = typeof body.error === 'object' ? body.error?.message : body.error_description;
|
|
104
|
-
const
|
|
225
|
+
const sanitizedMessage = spotifyMessage ? sanitizeOneLineText(spotifyMessage) : '';
|
|
226
|
+
const message = sanitizedMessage || `Spotify API request failed with HTTP ${response.status}.`;
|
|
105
227
|
if (response.status === 401) {
|
|
106
228
|
return new AuthenticationRequiredError('Spotify authentication has expired or was revoked.\n\nRun: spoti login');
|
|
107
229
|
}
|
|
108
230
|
if (response.status === 403 && /premium/i.test(message))
|
|
109
231
|
return new PremiumRequiredError();
|
|
110
|
-
if (response.status === 404 &&
|
|
232
|
+
if (response.status === 404 &&
|
|
233
|
+
PLAYBACK_CONTROL_REQUESTS.has(`${method} ${path}`) &&
|
|
234
|
+
/\b(?:no active device|device not found)\b/i.test(message)) {
|
|
111
235
|
return new NoActiveDeviceError();
|
|
236
|
+
}
|
|
112
237
|
if (response.status === 429) {
|
|
113
|
-
|
|
114
|
-
return new RateLimitedError(Number.isFinite(retryAfter) ? retryAfter : 1);
|
|
238
|
+
return new RateLimitedError(getRetryAfterSeconds(response));
|
|
115
239
|
}
|
|
116
240
|
return new SpotifyApiError(message, response.status);
|
|
117
241
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/spotify/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,2BAA2B,EAC3B,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,OAAO,GACR,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/spotify/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,2BAA2B,EAC3B,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,OAAO,GACR,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAClD,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACjC,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IACxC,gBAAgB;IAChB,qBAAqB;IACrB,sBAAsB;IACtB,sBAAsB;IACtB,0BAA0B;IAC1B,qBAAqB;IACrB,uBAAuB;IACvB,uBAAuB;IACvB,wBAAwB;IACxB,uBAAuB;CACxB,CAAC,CAAC;AAIH,MAAM,KAAK,GAAU,CAAC,YAAY,EAAE,EAAE,CACpC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;AAoB9D,MAAM,OAAO,aAAa;IAEL;IACA;IACA;IAHnB,YACmB,WAAgC,EAChC,UAAwB,KAAK,EAC7B,UAAiB,KAAK;QAFtB,gBAAW,GAAX,WAAW,CAAqB;QAChC,YAAO,GAAP,OAAO,CAAsB;QAC7B,YAAO,GAAP,OAAO,CAAe;IACtC,CAAC;IAEJ,GAAG,CAAI,IAAY,EAAE,OAAwB;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,CAAI,IAAY,EAAE,OAAwB;QAC5C,OAAO,IAAI,CAAC,OAAO,CAAI,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,GAAG,CAAI,IAAY,EAAE,OAAwB;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,CAAI,IAAY,EAAE,OAAwB;QAC9C,OAAO,IAAI,CAAC,OAAO,CAAI,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC;IAEO,KAAK,CAAC,OAAO,CACnB,MAAc,EACd,IAAY,EACZ,UAA0B,EAAE;QAE5B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;QAClF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;YAC/D,IAAI,KAAK,KAAK,SAAS;gBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAC9B,IAAI,IAAwB,CAAC;QAC7B,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YAChD,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC;QAED,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,WAAW,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;QAC1D,IAAI,cAAc,GAAG,KAAK,CAAC;QAC3B,IAAI,gBAAgB,GAAG,CAAC,CAAC;QAEzB,OAAO,IAAI,EAAE,CAAC;YACZ,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,WAAW,EAAE,CAAC,CAAC;YACtD,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACpD,IAAI,kBAAkB,GAAG,KAAK,CAAC;YAC/B,IAAI,YAAgC,CAAC;YAErC,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;oBACvC,MAAM;oBACN,OAAO;oBACP,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;iBACxC,CAAC,CAAC;gBAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;oBAC/C,kBAAkB,GAAG,IAAI,CAAC;gBAC5B,CAAC;qBAAM,IACL,QAAQ,CAAC,MAAM,KAAK,GAAG;oBACvB,gBAAgB,GAAG,sBAAsB,EACzC,CAAC;oBACD,MAAM,iBAAiB,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;oBACzD,MAAM,oBAAoB,GAAG,GAAG,GAAG,CAAC,IAAI,gBAAgB,CAAC;oBACzD,gBAAgB,IAAI,CAAC,CAAC;oBACtB,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,GAAG,KAAK,EAAE,oBAAoB,CAAC,CAAC;gBAC3E,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;wBACjB,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;wBAClE,qBAAqB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;wBACtD,MAAM,WAAW,CAAC;oBACpB,CAAC;oBACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;wBAAE,OAAO,SAAc,CAAC;oBAEnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACnC,qBAAqB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;oBACtD,IAAI,CAAC,IAAI;wBAAE,OAAO,SAAc,CAAC;oBAEjC,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;oBACpF,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;wBAClC,IAAI,MAAM,KAAK,KAAK;4BAAE,OAAO,SAAc,CAAC;wBAC5C,MAAM,IAAI,eAAe,CACvB,mDAAmD,EACnD,QAAQ,CAAC,MAAM,CAChB,CAAC;oBACJ,CAAC;oBAED,IAAI,CAAC;wBACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;oBAC/B,CAAC;oBAAC,MAAM,CAAC;wBACP,MAAM,IAAI,eAAe,CAAC,kCAAkC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;oBACjF,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACrE,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,CAAC;YAED,IAAI,kBAAkB,EAAE,CAAC;gBACvB,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACzC,WAAW,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,uBAAuB,EAAE,CAAC;gBAC/D,cAAc,GAAG,IAAI,CAAC;gBACtB,SAAS;YACX,CAAC;YAED,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC/B,MAAM,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;gBACtD,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,YAAoB,EAAE,MAAoB;QACnE,wBAAwB,CAAC,MAAM,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACjC,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC3B,MAAM,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,IAAI,aAAuC,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;YACtD,aAAa,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAChE,IAAI,MAAM,CAAC,OAAO;gBAAE,aAAa,EAAE,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5D,CAAC;gBAAS,CAAC;YACT,IAAI,aAAa;gBAAE,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;CACF;AAOD,SAAS,mBAAmB,CAAC,cAA4B;IACvD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,uBAAuB,GAAG,GAAG,EAAE;QACnC,IAAI,cAAc;YAAE,UAAU,CAAC,KAAK,CAAC,sBAAsB,CAAC,cAAc,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC;IACF,cAAc,EAAE,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACnF,IAAI,cAAc,EAAE,OAAO;QAAE,uBAAuB,EAAE,CAAC;IAEvD,MAAM,OAAO,GAAG,UAAU,CACxB,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,0BAA0B,EAAE,cAAc,CAAC,CAAC,EACpF,MAAM,CACP,CAAC;IACF,OAAO,CAAC,KAAK,EAAE,CAAC;IAEhB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,OAAO;QACL,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,SAAS;gBAAE,OAAO;YACtB,SAAS,GAAG,IAAI,CAAC;YACjB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,cAAc,EAAE,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;QACxE,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAAc,EACd,aAA0B,EAC1B,cAA4B;IAE5B,IAAI,cAAc,EAAE,OAAO;QAAE,OAAO,sBAAsB,CAAC,cAAc,CAAC,CAAC;IAE3E,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACvC,IACE,eAAe,CAAC,IAAI,KAAK,cAAc;QACvC,CAAC,aAAa,CAAC,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC,EAChF,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,uDAAuD,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,eAAe,YAAY,QAAQ;QAAE,OAAO,eAAe,CAAC;IAChE,OAAO,IAAI,QAAQ,CACjB,4BAA4B,mBAAmB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAC3E,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAC5B,aAA0B,EAC1B,cAA4B;IAE5B,wBAAwB,CAAC,cAAc,CAAC,CAAC;IACzC,IAAI,aAAa,CAAC,OAAO;QAAE,MAAM,aAAa,CAAC,MAAM,CAAC;AACxD,CAAC;AAED,SAAS,wBAAwB,CAAC,MAAoB;IACpD,IAAI,MAAM,EAAE,OAAO;QAAE,MAAM,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,sBAAsB,CAAC,MAAmB;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,MAAM,YAAY,KAAK,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,MAAM,CAAC;IAC3E,OAAO,IAAI,YAAY,CAAC,4BAA4B,EAAE,YAAY,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,cAAc,CAAC,YAAoB,EAAE,MAAmB;IAC/D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;YACnD,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,YAAY,CAAC,CAAC;QACjB,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,OAAO;YAAE,aAAa,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,oBAAoB,CAAC,QAAkB;IAC9C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACnD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC;IAE9B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACvD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,QAAkB,EAClB,MAAc,EACd,IAAY;IAEZ,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,cAAc,GAClB,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;IAChF,MAAM,gBAAgB,GAAG,cAAc,CAAC,CAAC,CAAC,mBAAmB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,MAAM,OAAO,GACX,gBAAgB,IAAI,wCAAwC,QAAQ,CAAC,MAAM,GAAG,CAAC;IAEjF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,2BAA2B,CACpC,wEAAwE,CACzE,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,oBAAoB,EAAE,CAAC;IAC3F,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;QACvB,yBAAyB,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;QAClD,4CAA4C,CAAC,IAAI,CAAC,OAAO,CAAC,EAC1D,CAAC;QACD,OAAO,IAAI,mBAAmB,EAAE,CAAC;IACnC,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,gBAAgB,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,IAAI,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,QAAkB;IAC7C,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAqB,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/dist/spotify/types.d.ts
CHANGED
|
@@ -122,6 +122,8 @@ export interface SpotifyCursorPaging<T> {
|
|
|
122
122
|
}
|
|
123
123
|
export interface SpotifyPlaybackState {
|
|
124
124
|
is_playing: boolean;
|
|
125
|
+
repeat_state: string;
|
|
126
|
+
shuffle_state: boolean;
|
|
125
127
|
progress_ms: number | null;
|
|
126
128
|
item: SpotifyPlaybackItem | null;
|
|
127
129
|
device: {
|
package/dist/ui/completions.js
CHANGED
|
@@ -77,16 +77,16 @@ _spoti() {
|
|
|
77
77
|
'seek:seek within the current track'
|
|
78
78
|
'volume:set or adjust the active device volume'
|
|
79
79
|
'queue:show the playback queue or add a searched track'
|
|
80
|
-
'shuffle:
|
|
81
|
-
'repeat:
|
|
80
|
+
'shuffle:show or change the playback shuffle state'
|
|
81
|
+
'repeat:show or change the playback repeat mode'
|
|
82
82
|
'album:search for and show an album'
|
|
83
83
|
'artist:search for and show an artist'
|
|
84
|
-
'playlists:list your Spotify playlists'
|
|
84
|
+
'playlists:list and optionally play your Spotify playlists'
|
|
85
85
|
'playlist:show one of your Spotify playlists'
|
|
86
|
-
'liked:list your liked tracks'
|
|
86
|
+
'liked:list and optionally play your liked tracks'
|
|
87
87
|
'like:add the current track to your Spotify library'
|
|
88
88
|
'unlike:remove the current track from your Spotify library'
|
|
89
|
-
'recent:show recently played tracks'
|
|
89
|
+
'recent:show and optionally play recently played tracks'
|
|
90
90
|
'update:check for or install the latest spoti version'
|
|
91
91
|
'search:search Spotify tracks'
|
|
92
92
|
'play:play a track, album, artist, or playlist'
|
|
@@ -191,16 +191,16 @@ complete -c spoti -n __spoti_needs_command -a device -d 'Transfer playback to a
|
|
|
191
191
|
complete -c spoti -n __spoti_needs_command -a seek -d 'Seek within the current track'
|
|
192
192
|
complete -c spoti -n __spoti_needs_command -a volume -d 'Set or adjust volume'
|
|
193
193
|
complete -c spoti -n __spoti_needs_command -a queue -d 'Show or add to the queue'
|
|
194
|
-
complete -c spoti -n __spoti_needs_command -a shuffle -d '
|
|
195
|
-
complete -c spoti -n __spoti_needs_command -a repeat -d '
|
|
194
|
+
complete -c spoti -n __spoti_needs_command -a shuffle -d 'Show or change playback shuffle state'
|
|
195
|
+
complete -c spoti -n __spoti_needs_command -a repeat -d 'Show or change playback repeat mode'
|
|
196
196
|
complete -c spoti -n __spoti_needs_command -a album -d 'Search for an album'
|
|
197
197
|
complete -c spoti -n __spoti_needs_command -a artist -d 'Search for an artist'
|
|
198
|
-
complete -c spoti -n __spoti_needs_command -a playlists -d 'List your playlists'
|
|
198
|
+
complete -c spoti -n __spoti_needs_command -a playlists -d 'List and optionally play your playlists'
|
|
199
199
|
complete -c spoti -n __spoti_needs_command -a playlist -d 'Show a playlist'
|
|
200
|
-
complete -c spoti -n __spoti_needs_command -a liked -d 'List liked tracks'
|
|
200
|
+
complete -c spoti -n __spoti_needs_command -a liked -d 'List and optionally play liked tracks'
|
|
201
201
|
complete -c spoti -n __spoti_needs_command -a like -d 'Like the current track'
|
|
202
202
|
complete -c spoti -n __spoti_needs_command -a unlike -d 'Unlike the current track'
|
|
203
|
-
complete -c spoti -n __spoti_needs_command -a recent -d 'Show recently played tracks'
|
|
203
|
+
complete -c spoti -n __spoti_needs_command -a recent -d 'Show and optionally play recently played tracks'
|
|
204
204
|
complete -c spoti -n __spoti_needs_command -a update -d 'Check for or install updates'
|
|
205
205
|
complete -c spoti -n __spoti_needs_command -a search -d 'Search Spotify tracks'
|
|
206
206
|
complete -c spoti -n __spoti_needs_command -a play -d 'Play a track, album, artist, or playlist'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlayerService } from '../services/player.service.js';
|
|
2
2
|
import type { SearchService } from '../services/search.service.js';
|
|
3
|
+
import { type OutputStyles } from './output.js';
|
|
3
4
|
export type SearchCategory = 'track' | 'album' | 'artist' | 'playlist';
|
|
4
5
|
export type InteractiveSearchKey = {
|
|
5
6
|
type: 'character';
|
|
@@ -11,7 +12,7 @@ export interface InteractiveSearchTerminal {
|
|
|
11
12
|
readonly isTTY: boolean;
|
|
12
13
|
setup(): void;
|
|
13
14
|
cleanup(): void;
|
|
14
|
-
readKey(): Promise<InteractiveSearchKey>;
|
|
15
|
+
readKey(signal?: AbortSignal): Promise<InteractiveSearchKey>;
|
|
15
16
|
write(value: string): void;
|
|
16
17
|
}
|
|
17
18
|
export interface InteractiveSearchServices {
|
|
@@ -21,6 +22,7 @@ export interface InteractiveSearchServices {
|
|
|
21
22
|
export interface InteractiveSearchOptions extends InteractiveSearchServices {
|
|
22
23
|
terminal?: InteractiveSearchTerminal;
|
|
23
24
|
limit?: number;
|
|
25
|
+
styles?: OutputStyles;
|
|
24
26
|
}
|
|
25
27
|
export type InteractiveSearchResult = {
|
|
26
28
|
status: 'not-interactive' | 'cancelled';
|
|
@@ -28,6 +30,7 @@ export type InteractiveSearchResult = {
|
|
|
28
30
|
status: 'played';
|
|
29
31
|
category: SearchCategory;
|
|
30
32
|
uri: string;
|
|
33
|
+
label: string;
|
|
31
34
|
};
|
|
32
35
|
export declare function runInteractiveSearch(options: InteractiveSearchOptions): Promise<InteractiveSearchResult>;
|
|
33
36
|
export declare function createNodeInteractiveSearchTerminal(): InteractiveSearchTerminal;
|