upnext-adapter-spotify 0.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.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +224 -0
  3. package/dist/src/applescript.d.ts +100 -0
  4. package/dist/src/applescript.d.ts.map +1 -0
  5. package/dist/src/applescript.js +197 -0
  6. package/dist/src/applescript.js.map +1 -0
  7. package/dist/src/desktop.d.ts +100 -0
  8. package/dist/src/desktop.d.ts.map +1 -0
  9. package/dist/src/desktop.js +194 -0
  10. package/dist/src/desktop.js.map +1 -0
  11. package/dist/src/errors.d.ts +61 -0
  12. package/dist/src/errors.d.ts.map +1 -0
  13. package/dist/src/errors.js +93 -0
  14. package/dist/src/errors.js.map +1 -0
  15. package/dist/src/http.d.ts +50 -0
  16. package/dist/src/http.d.ts.map +1 -0
  17. package/dist/src/http.js +113 -0
  18. package/dist/src/http.js.map +1 -0
  19. package/dist/src/index.d.ts +34 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +28 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/metadata.d.ts +39 -0
  24. package/dist/src/metadata.d.ts.map +1 -0
  25. package/dist/src/metadata.js +130 -0
  26. package/dist/src/metadata.js.map +1 -0
  27. package/dist/src/ref.d.ts +29 -0
  28. package/dist/src/ref.d.ts.map +1 -0
  29. package/dist/src/ref.js +150 -0
  30. package/dist/src/ref.js.map +1 -0
  31. package/dist/src/sampler.d.ts +68 -0
  32. package/dist/src/sampler.d.ts.map +1 -0
  33. package/dist/src/sampler.js +96 -0
  34. package/dist/src/sampler.js.map +1 -0
  35. package/dist/src/uri.d.ts +31 -0
  36. package/dist/src/uri.d.ts.map +1 -0
  37. package/dist/src/uri.js +92 -0
  38. package/dist/src/uri.js.map +1 -0
  39. package/dist/src/watch.d.ts +35 -0
  40. package/dist/src/watch.d.ts.map +1 -0
  41. package/dist/src/watch.js +131 -0
  42. package/dist/src/watch.js.map +1 -0
  43. package/dist/src/web.d.ts +115 -0
  44. package/dist/src/web.d.ts.map +1 -0
  45. package/dist/src/web.js +300 -0
  46. package/dist/src/web.js.map +1 -0
  47. package/package.json +42 -0
  48. package/src/applescript.ts +241 -0
  49. package/src/desktop.ts +250 -0
  50. package/src/errors.ts +119 -0
  51. package/src/http.ts +159 -0
  52. package/src/index.ts +51 -0
  53. package/src/metadata.ts +167 -0
  54. package/src/ref.ts +145 -0
  55. package/src/sampler.ts +173 -0
  56. package/src/uri.ts +104 -0
  57. package/src/watch.ts +145 -0
  58. package/src/web.ts +343 -0
@@ -0,0 +1,115 @@
1
+ import { type Scheduler } from 'upnext-core';
2
+ import type { Adapter, AdapterEvent, Binding, Capabilities, MediaRef } from 'upnext-core';
3
+ import type { Sample } from './applescript.js';
4
+ import { type SpotifyHttpOptions, type TokenProvider } from './http.js';
5
+ export interface SpotifyWebOptions extends SpotifyHttpOptions {
6
+ id?: string;
7
+ /**
8
+ * Which device to command. Without one Spotify targets whatever is currently
9
+ * active, which is usually right and occasionally surprising — a phone that
10
+ * woke up last is as "active" as the laptop in front of you.
11
+ */
12
+ deviceId?: string;
13
+ /**
14
+ * How often to read player state while something is loaded.
15
+ *
16
+ * Slower than the desktop backend's default on purpose: every sample is a
17
+ * real API call against a shared rate limit, and the host is using that same
18
+ * budget for its own work. Two seconds is still well inside what a person
19
+ * would call responsive for a progress bar.
20
+ */
21
+ sampleIntervalMs?: number;
22
+ scheduler?: Scheduler;
23
+ /** ISO 3166-1 alpha-2, so search and lookups return what this listener can play. */
24
+ market?: string;
25
+ }
26
+ /**
27
+ * Plays Spotify through the Web API, anywhere Node runs.
28
+ *
29
+ * The counterpart to `SpotifyDesktopAdapter`, and a deliberate demonstration of
30
+ * why capabilities are a per-adapter fact rather than a per-service one: these
31
+ * two drive the same service and are not interchangeable. This one can search a
32
+ * catalogue of a hundred million tracks and runs on Linux; it also needs an
33
+ * OAuth token, a Premium account and a device that is awake. The desktop one
34
+ * needs none of that and cannot search at all.
35
+ *
36
+ * An agent does not have to know any of that. It asks `runtime.can('search')`
37
+ * and gets the truth for whichever one is loaded.
38
+ *
39
+ * What the host has to provide:
40
+ *
41
+ * - `getAccessToken`, returning a user token with `user-read-playback-state`
42
+ * and `user-modify-playback-state`. This library never runs an OAuth flow.
43
+ * - A Spotify Premium account. Every playback-control endpoint is Premium-only
44
+ * and there is nothing an adapter can do about that; a free account gets a
45
+ * `premium-required` failure rather than a mystery.
46
+ * - Somewhere to play. The Web API commands a device, it is not one — so the
47
+ * Spotify app has to be open somewhere, or `deviceId` has to name something
48
+ * real. `no-device` is the failure when it is not.
49
+ */
50
+ export declare class SpotifyWebAdapter implements Adapter {
51
+ #private;
52
+ readonly id: string;
53
+ readonly capabilities: Capabilities;
54
+ constructor(options: SpotifyWebOptions);
55
+ /**
56
+ * Prove the token works before the first song rather than during it.
57
+ *
58
+ * Reading player state is the right probe because it needs exactly the scope
59
+ * everything else here needs, so a token that passes this has been checked
60
+ * against the real requirement rather than a cheaper one. `204` — nothing
61
+ * playing — is a pass: it means the call was authorised, which is all this
62
+ * is asking.
63
+ */
64
+ init(): Promise<void>;
65
+ match(ref: MediaRef): number;
66
+ resolve(ref: MediaRef): Promise<Binding | null>;
67
+ search(query: string, limit?: number): Promise<MediaRef[]>;
68
+ /**
69
+ * Everything inside an album or a playlist, as refs.
70
+ *
71
+ * Not part of the `Adapter` contract, because a container is not a queue
72
+ * entry — this runtime holds one item at a time and owns the ordering itself.
73
+ * "Play my Discover Weekly" is therefore two steps, and they are two steps on
74
+ * purpose: the host gets to see, filter and reorder the list before it becomes
75
+ * a queue, rather than having thirty entries appear because one URI was
76
+ * enqueued.
77
+ *
78
+ * const tracks = await spotify.expandContext('spotify:playlist:37i9…');
79
+ * runtime.enqueueMany(tracks);
80
+ */
81
+ expandContext(uri: string, limit?: number): Promise<MediaRef[]>;
82
+ load(binding: Binding, opts?: {
83
+ startAtMs?: number;
84
+ }): Promise<void>;
85
+ play(): Promise<void>;
86
+ pause(): Promise<void>;
87
+ /**
88
+ * The Web API has no stop, so this pauses — and tolerates being told it could
89
+ * not. The runtime stops the outgoing backend on every transition, and by the
90
+ * time that lands the device may already have moved on or gone to sleep;
91
+ * Spotify answers that with a 403 "restriction violated". Failing a queue
92
+ * transition because the thing we were stopping had already stopped would be
93
+ * absurd.
94
+ */
95
+ stop(): Promise<void>;
96
+ seek(positionMs: number): Promise<void>;
97
+ setVolume(volume: number): Promise<void>;
98
+ subscribe(listener: (event: AdapterEvent) => void): () => void;
99
+ dispose(): Promise<void>;
100
+ }
101
+ /**
102
+ * Player state as a `Sample`, so the Web API and the desktop app run through
103
+ * the same interpretation.
104
+ *
105
+ * The two backends fail in different ways but the *question* is identical —
106
+ * did our track end, or did a person take over — and it is subtle enough that
107
+ * having one answer to it, tested once, is worth the small mapping here.
108
+ *
109
+ * A `204` (which arrives as `null`) means no active device: nothing is playing
110
+ * and there is nowhere for it to play. That is the same situation as the
111
+ * desktop app being closed, so it maps to the same `running: false`.
112
+ */
113
+ export declare function toSample(body: unknown): Sample;
114
+ export type { TokenProvider };
115
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EAAe,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAKrF,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,iBAAkB,YAAW,OAAO;;IAC/C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,QAAQ,CAAC,YAAY,EAAE,YAAY,CASjC;gBAYU,OAAO,EAAE,iBAAiB;IAkBtC;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM;IAUtB,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAgB/C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAO5D;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAwB5D,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMvC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAmC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAoB9C;AAqBD,YAAY,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,300 @@
1
+ import { defaultCapabilities, systemScheduler } from 'upnext-core';
2
+ import { SpotifyError } from './errors.js';
3
+ import { SpotifyHttp } from './http.js';
4
+ import { readTrack, searchQuery } from './ref.js';
5
+ import { isPlayableKind, parseSpotifyUri } from './uri.js';
6
+ import { BackendWatcher } from './watch.js';
7
+ /**
8
+ * Plays Spotify through the Web API, anywhere Node runs.
9
+ *
10
+ * The counterpart to `SpotifyDesktopAdapter`, and a deliberate demonstration of
11
+ * why capabilities are a per-adapter fact rather than a per-service one: these
12
+ * two drive the same service and are not interchangeable. This one can search a
13
+ * catalogue of a hundred million tracks and runs on Linux; it also needs an
14
+ * OAuth token, a Premium account and a device that is awake. The desktop one
15
+ * needs none of that and cannot search at all.
16
+ *
17
+ * An agent does not have to know any of that. It asks `runtime.can('search')`
18
+ * and gets the truth for whichever one is loaded.
19
+ *
20
+ * What the host has to provide:
21
+ *
22
+ * - `getAccessToken`, returning a user token with `user-read-playback-state`
23
+ * and `user-modify-playback-state`. This library never runs an OAuth flow.
24
+ * - A Spotify Premium account. Every playback-control endpoint is Premium-only
25
+ * and there is nothing an adapter can do about that; a free account gets a
26
+ * `premium-required` failure rather than a mystery.
27
+ * - Somewhere to play. The Web API commands a device, it is not one — so the
28
+ * Spotify app has to be open somewhere, or `deviceId` has to name something
29
+ * real. `no-device` is the failure when it is not.
30
+ */
31
+ export class SpotifyWebAdapter {
32
+ id;
33
+ capabilities = {
34
+ ...defaultCapabilities,
35
+ endOfTrack: 'event',
36
+ position: 'authoritative',
37
+ externalControl: true,
38
+ seek: true,
39
+ pause: true,
40
+ volume: true,
41
+ search: true,
42
+ };
43
+ #http;
44
+ #deviceId;
45
+ #market;
46
+ #watcher;
47
+ #listeners = new Set();
48
+ #binding = null;
49
+ #startAtMs = 0;
50
+ #started = false;
51
+ constructor(options) {
52
+ this.id = options.id ?? 'spotify-web';
53
+ this.#http = new SpotifyHttp(options);
54
+ this.#deviceId = options.deviceId;
55
+ this.#market = options.market;
56
+ const intervalMs = options.sampleIntervalMs ?? 2000;
57
+ this.#watcher = new BackendWatcher({
58
+ read: async () => toSample(await this.#http.request('GET', '/me/player', { query: { market: this.#market } })),
59
+ emit: (event) => this.#emit(event),
60
+ scheduler: options.scheduler ?? systemScheduler,
61
+ intervalMs,
62
+ rolloverWindowMs: Math.max(3000, intervalMs * 2),
63
+ confirmWithin: Math.max(3, Math.ceil(6000 / intervalMs)),
64
+ });
65
+ }
66
+ /**
67
+ * Prove the token works before the first song rather than during it.
68
+ *
69
+ * Reading player state is the right probe because it needs exactly the scope
70
+ * everything else here needs, so a token that passes this has been checked
71
+ * against the real requirement rather than a cheaper one. `204` — nothing
72
+ * playing — is a pass: it means the call was authorised, which is all this
73
+ * is asking.
74
+ */
75
+ async init() {
76
+ await this.#http.request('GET', '/me/player');
77
+ }
78
+ match(ref) {
79
+ const parsed = parseSpotifyUri(ref.uri);
80
+ if (parsed)
81
+ return isPlayableKind(parsed.kind) ? 1 : 0;
82
+ // An ISRC is a recording id, so a search on it lands on the same recording
83
+ // rather than something with a similar name — nearly as good as a URI.
84
+ if (ref.isrc)
85
+ return 0.9;
86
+ if (ref.title)
87
+ return 0.65;
88
+ return 0;
89
+ }
90
+ async resolve(ref) {
91
+ const parsed = parseSpotifyUri(ref.uri);
92
+ const found = parsed ? await this.#byId(parsed) : await this.#bySearch(ref);
93
+ if (!found?.uri)
94
+ return null;
95
+ return {
96
+ adapterId: this.id,
97
+ nativeUri: found.uri,
98
+ // Spotify's copy wins here, unlike the desktop backend: this came from the
99
+ // catalogue itself, so its duration and its ISRC are authoritative in a
100
+ // way a caller's guess at a title is not. The caller's title survives only
101
+ // where Spotify had nothing.
102
+ ref: { ...ref, ...found },
103
+ };
104
+ }
105
+ async search(query, limit = 10) {
106
+ const body = await this.#http.request('GET', '/search', {
107
+ query: { q: query, type: 'track', limit, market: this.#market },
108
+ });
109
+ return readTrackList(readPath(body, ['tracks', 'items']));
110
+ }
111
+ /**
112
+ * Everything inside an album or a playlist, as refs.
113
+ *
114
+ * Not part of the `Adapter` contract, because a container is not a queue
115
+ * entry — this runtime holds one item at a time and owns the ordering itself.
116
+ * "Play my Discover Weekly" is therefore two steps, and they are two steps on
117
+ * purpose: the host gets to see, filter and reorder the list before it becomes
118
+ * a queue, rather than having thirty entries appear because one URI was
119
+ * enqueued.
120
+ *
121
+ * const tracks = await spotify.expandContext('spotify:playlist:37i9…');
122
+ * runtime.enqueueMany(tracks);
123
+ */
124
+ async expandContext(uri, limit = 100) {
125
+ const parsed = parseSpotifyUri(uri);
126
+ if (!parsed)
127
+ return [];
128
+ if (parsed.kind === 'album') {
129
+ const body = await this.#http.request('GET', `/albums/${parsed.id}/tracks`, {
130
+ query: { limit, market: this.#market },
131
+ });
132
+ return readTrackList(readPath(body, ['items']));
133
+ }
134
+ if (parsed.kind === 'playlist') {
135
+ const body = await this.#http.request('GET', `/playlists/${parsed.id}/tracks`, {
136
+ query: { limit, market: this.#market },
137
+ });
138
+ const items = readPath(body, ['items']);
139
+ // A playlist wraps each track in an entry that also carries who added it
140
+ // and when; the track itself is one level down.
141
+ return Array.isArray(items)
142
+ ? readTrackList(items.map((entry) => readPath(entry, ['track'])))
143
+ : [];
144
+ }
145
+ return [];
146
+ }
147
+ async load(binding, opts) {
148
+ this.#watcher.stop();
149
+ this.#binding = binding;
150
+ this.#startAtMs = opts?.startAtMs ?? 0;
151
+ this.#started = false;
152
+ }
153
+ async play() {
154
+ const binding = this.#binding;
155
+ if (!binding)
156
+ throw new SpotifyError('failed', `${this.id}: nothing is loaded`);
157
+ // With no body this is "resume"; with `uris` it is "play exactly this".
158
+ // Handing it a single URI rather than a context is what keeps Spotify from
159
+ // rolling into an album we did not queue when the track ends.
160
+ const body = this.#started
161
+ ? undefined
162
+ : { uris: [binding.nativeUri], ...(this.#startAtMs > 0 ? { position_ms: this.#startAtMs } : {}) };
163
+ await this.#http.request('PUT', '/me/player/play', {
164
+ query: { device_id: this.#deviceId },
165
+ ...(body ? { body } : {}),
166
+ });
167
+ this.#started = true;
168
+ this.#watcher.start(binding.nativeUri);
169
+ }
170
+ async pause() {
171
+ await this.#http.request('PUT', '/me/player/pause', {
172
+ query: { device_id: this.#deviceId },
173
+ });
174
+ }
175
+ /**
176
+ * The Web API has no stop, so this pauses — and tolerates being told it could
177
+ * not. The runtime stops the outgoing backend on every transition, and by the
178
+ * time that lands the device may already have moved on or gone to sleep;
179
+ * Spotify answers that with a 403 "restriction violated". Failing a queue
180
+ * transition because the thing we were stopping had already stopped would be
181
+ * absurd.
182
+ */
183
+ async stop() {
184
+ this.#watcher.stop();
185
+ const wasStarted = this.#started;
186
+ this.#binding = null;
187
+ this.#started = false;
188
+ if (!wasStarted)
189
+ return;
190
+ await this.#http.request('PUT', '/me/player/pause', {
191
+ query: { device_id: this.#deviceId },
192
+ tolerate: [403, 404],
193
+ });
194
+ }
195
+ async seek(positionMs) {
196
+ await this.#http.request('PUT', '/me/player/seek', {
197
+ query: { position_ms: Math.max(0, Math.round(positionMs)), device_id: this.#deviceId },
198
+ });
199
+ }
200
+ async setVolume(volume) {
201
+ await this.#http.request('PUT', '/me/player/volume', {
202
+ query: {
203
+ volume_percent: Math.min(100, Math.max(0, Math.round(volume * 100))),
204
+ device_id: this.#deviceId,
205
+ },
206
+ });
207
+ }
208
+ subscribe(listener) {
209
+ this.#listeners.add(listener);
210
+ return () => this.#listeners.delete(listener);
211
+ }
212
+ async dispose() {
213
+ this.#watcher.stop();
214
+ this.#listeners.clear();
215
+ this.#binding = null;
216
+ }
217
+ // -- resolution -----------------------------------------------------------
218
+ async #byId(parsed) {
219
+ if (!isPlayableKind(parsed.kind))
220
+ return null;
221
+ const path = parsed.kind === 'episode' ? `/episodes/${parsed.id}` : `/tracks/${parsed.id}`;
222
+ try {
223
+ return readTrack(await this.#http.request('GET', path, { query: { market: this.#market } }));
224
+ }
225
+ catch (err) {
226
+ // A track Spotify does not have, or does not have here, is this adapter
227
+ // answering "not me" — the runtime should try another source rather than
228
+ // treat the entry as broken.
229
+ if (err instanceof SpotifyError && err.reason === 'not-found')
230
+ return null;
231
+ throw err;
232
+ }
233
+ }
234
+ async #bySearch(ref) {
235
+ const query = searchQuery(ref);
236
+ if (!query)
237
+ return null;
238
+ const hits = await this.search(query, 5);
239
+ // The runtime scores this against what was asked for and rejects it if the
240
+ // match is too loose, so returning the top hit is safe: a wrong guess is
241
+ // caught one layer up rather than played.
242
+ return hits[0] ?? null;
243
+ }
244
+ #emit(event) {
245
+ for (const listener of [...this.#listeners])
246
+ listener(event);
247
+ }
248
+ }
249
+ /**
250
+ * Player state as a `Sample`, so the Web API and the desktop app run through
251
+ * the same interpretation.
252
+ *
253
+ * The two backends fail in different ways but the *question* is identical —
254
+ * did our track end, or did a person take over — and it is subtle enough that
255
+ * having one answer to it, tested once, is worth the small mapping here.
256
+ *
257
+ * A `204` (which arrives as `null`) means no active device: nothing is playing
258
+ * and there is nowhere for it to play. That is the same situation as the
259
+ * desktop app being closed, so it maps to the same `running: false`.
260
+ */
261
+ export function toSample(body) {
262
+ if (!body || typeof body !== 'object') {
263
+ return { running: false, status: 'idle', positionMs: 0, durationMs: null, nativeUri: null, volume: null };
264
+ }
265
+ const state = body;
266
+ const item = state.item && typeof state.item === 'object' ? state.item : null;
267
+ const durationMs = typeof item?.duration_ms === 'number' ? item.duration_ms : null;
268
+ const progress = typeof state.progress_ms === 'number' ? state.progress_ms : 0;
269
+ const device = state.device && typeof state.device === 'object' ? state.device : null;
270
+ const volume = typeof device?.volume_percent === 'number' ? device.volume_percent : null;
271
+ return {
272
+ running: true,
273
+ status: state.is_playing === true ? 'playing' : 'paused',
274
+ positionMs: Number.isFinite(progress) && progress > 0 ? Math.round(progress) : 0,
275
+ durationMs: durationMs !== null && Number.isFinite(durationMs) && durationMs > 0 ? Math.round(durationMs) : null,
276
+ nativeUri: typeof item?.uri === 'string' ? item.uri : null,
277
+ volume: volume === null ? null : Math.min(1, Math.max(0, volume / 100)),
278
+ };
279
+ }
280
+ function readTrackList(value) {
281
+ if (!Array.isArray(value))
282
+ return [];
283
+ const out = [];
284
+ for (const entry of value) {
285
+ const ref = readTrack(entry);
286
+ if (ref)
287
+ out.push(ref);
288
+ }
289
+ return out;
290
+ }
291
+ function readPath(value, keys) {
292
+ let current = value;
293
+ for (const key of keys) {
294
+ if (!current || typeof current !== 'object')
295
+ return undefined;
296
+ current = current[key];
297
+ }
298
+ return current;
299
+ }
300
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "upnext-adapter-spotify",
3
+ "version": "0.1.0",
4
+ "description": "Play Spotify through the desktop app on macOS with no credentials, or through the Web API with a token your host already holds.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/src/index.js",
8
+ "types": "./dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/src/index.js"
11
+ },
12
+ "files": [
13
+ "dist/src",
14
+ "src"
15
+ ],
16
+ "dependencies": {
17
+ "upnext-core": "0.1.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/tothienbao6a0/upnext.git",
25
+ "directory": "packages/adapter-spotify"
26
+ },
27
+ "homepage": "https://github.com/tothienbao6a0/upnext#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/tothienbao6a0/upnext/issues"
30
+ },
31
+ "keywords": [
32
+ "spotify",
33
+ "audio",
34
+ "playback",
35
+ "applescript",
36
+ "spotify-web-api",
37
+ "upnext-adapter"
38
+ ],
39
+ "devDependencies": {
40
+ "upnext-adapter-local": "0.1.0"
41
+ }
42
+ }