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.
- package/LICENSE +201 -0
- package/README.md +224 -0
- package/dist/src/applescript.d.ts +100 -0
- package/dist/src/applescript.d.ts.map +1 -0
- package/dist/src/applescript.js +197 -0
- package/dist/src/applescript.js.map +1 -0
- package/dist/src/desktop.d.ts +100 -0
- package/dist/src/desktop.d.ts.map +1 -0
- package/dist/src/desktop.js +194 -0
- package/dist/src/desktop.js.map +1 -0
- package/dist/src/errors.d.ts +61 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +93 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/http.d.ts +50 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +113 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +34 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +28 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/metadata.d.ts +39 -0
- package/dist/src/metadata.d.ts.map +1 -0
- package/dist/src/metadata.js +130 -0
- package/dist/src/metadata.js.map +1 -0
- package/dist/src/ref.d.ts +29 -0
- package/dist/src/ref.d.ts.map +1 -0
- package/dist/src/ref.js +150 -0
- package/dist/src/ref.js.map +1 -0
- package/dist/src/sampler.d.ts +68 -0
- package/dist/src/sampler.d.ts.map +1 -0
- package/dist/src/sampler.js +96 -0
- package/dist/src/sampler.js.map +1 -0
- package/dist/src/uri.d.ts +31 -0
- package/dist/src/uri.d.ts.map +1 -0
- package/dist/src/uri.js +92 -0
- package/dist/src/uri.js.map +1 -0
- package/dist/src/watch.d.ts +35 -0
- package/dist/src/watch.d.ts.map +1 -0
- package/dist/src/watch.js +131 -0
- package/dist/src/watch.js.map +1 -0
- package/dist/src/web.d.ts +115 -0
- package/dist/src/web.d.ts.map +1 -0
- package/dist/src/web.js +300 -0
- package/dist/src/web.js.map +1 -0
- package/package.json +42 -0
- package/src/applescript.ts +241 -0
- package/src/desktop.ts +250 -0
- package/src/errors.ts +119 -0
- package/src/http.ts +159 -0
- package/src/index.ts +51 -0
- package/src/metadata.ts +167 -0
- package/src/ref.ts +145 -0
- package/src/sampler.ts +173 -0
- package/src/uri.ts +104 -0
- package/src/watch.ts +145 -0
- 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"}
|
package/dist/src/web.js
ADDED
|
@@ -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
|
+
}
|