@spicetify/kit 0.3.1 → 0.4.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 +82 -0
- package/dist/args.js +15 -0
- package/dist/cli.js +4 -1
- package/dist/create.js +9 -5
- package/dist/dev.js +86 -22
- package/dist/install.js +4 -6
- package/dist/launch.js +24 -24
- package/dist/push.js +187 -44
- package/dist/remove.js +37 -0
- package/dist/stdlib-boundary.js +2 -2
- package/dist/version.js +1 -1
- package/package.json +6 -4
- package/vendor/classmaps/1030000/classmap-19f856aefd5.json +109 -0
- package/vendor/stdlib/metadata.json +1 -1
- package/vendor/stdlib/query.ts +31 -0
- package/vendor/stdlib/src/client.ts +89 -1
- package/vendor/stdlib/src/events.mix.ts +9 -0
- package/vendor/stdlib/src/events.ts +1 -1
- package/vendor/stdlib/src/registers/menu-order.ts +17 -0
- package/vendor/stdlib/src/registers/menu.ts +8 -4
- package/vendor/stdlib/src/registers/mount.ts +35 -9
- package/vendor/stdlib/src/registers/root.ts +1 -1
- package/vendor/stdlib/src/storage.ts +1 -1
- package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +3 -9
- package/vendor/stdlib/src/webpack/URI.xpui.ts +1 -1
- package/vendor/stdlib/src/webpack/capture-readiness.ts +5 -3
- package/vendor/stdlib/src/webpack/index.ts +10 -4
- package/vendor/stdlib/src/webpack/playlist-menu.ts +54 -0
- package/vendor/stdlib/src/wpunpk.mix.ts +1 -1
- package/vendor/classmaps/1020099/classmap-19f856aefd5.json +0 -109
package/dist/push.js
CHANGED
|
@@ -92,15 +92,140 @@ export function stampRecord(rec, id, nonce) {
|
|
|
92
92
|
export function newNonce() {
|
|
93
93
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
94
94
|
}
|
|
95
|
+
export const DEFAULT_PORT = "9229";
|
|
96
|
+
// resolvePort picks the CDP port: an explicit --port, then SPICETIFY_CDP_PORT
|
|
97
|
+
// (shared with the monorepo's maintainer scripts), then the default.
|
|
98
|
+
export function resolvePort(flag, env = process.env) {
|
|
99
|
+
return flag ?? env.SPICETIFY_CDP_PORT ?? DEFAULT_PORT;
|
|
100
|
+
}
|
|
101
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
102
|
+
// classifyDevtools decides who owns a DevTools endpoint from its /json/version
|
|
103
|
+
// and /json/list bodies. Spotify's CEF user agent carries "Spotify/<version>";
|
|
104
|
+
// anything else (the Node inspector, wrangler, a browser) is a foreign listener
|
|
105
|
+
// that would make Spotify fail to bind the port.
|
|
106
|
+
export function classifyDevtools(version, targets) {
|
|
107
|
+
if (!/Spotify\//.test(String(version["User-Agent"] ?? ""))) {
|
|
108
|
+
const first = targets[0];
|
|
109
|
+
const what = String(version.Browser || first?.title || first?.type || "an unknown DevTools endpoint");
|
|
110
|
+
return { state: "other", what };
|
|
111
|
+
}
|
|
112
|
+
for (const t of targets) {
|
|
113
|
+
if (typeof t.url === "string" && t.url.includes("xpui") && typeof t.webSocketDebuggerUrl === "string") {
|
|
114
|
+
return { state: "xpui", ws: t.webSocketDebuggerUrl };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { state: "spotify" };
|
|
118
|
+
}
|
|
119
|
+
// probePort reports whether the port is free, held by Spotify (with or without
|
|
120
|
+
// its xpui page loaded yet), held by something else, or answering in a way it
|
|
121
|
+
// cannot identify (a non-HTTP listener, or a client still binding the port).
|
|
122
|
+
export async function probePort(port) {
|
|
123
|
+
let version;
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(`http://localhost:${port}/json/version`, { signal: AbortSignal.timeout(2_000) });
|
|
126
|
+
if (!res.ok)
|
|
127
|
+
return { state: "unknown" };
|
|
128
|
+
const body = await res.json();
|
|
129
|
+
version = isRecord(body) ? body : {};
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
const code = e.cause?.code;
|
|
133
|
+
return code === "ECONNREFUSED" ? { state: "free" } : { state: "unknown" };
|
|
134
|
+
}
|
|
135
|
+
let targets = [];
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(`http://localhost:${port}/json/list`, { signal: AbortSignal.timeout(2_000) });
|
|
138
|
+
const body = res.ok ? await res.json() : [];
|
|
139
|
+
if (Array.isArray(body))
|
|
140
|
+
targets = body.filter(isRecord);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* version answered; an unreadable list classifies from the version alone */
|
|
144
|
+
}
|
|
145
|
+
return classifyDevtools(version, targets);
|
|
146
|
+
}
|
|
147
|
+
export function portInUseMessage(port, what) {
|
|
148
|
+
return (`port ${port} is already in use by ${what}, not Spotify. ` +
|
|
149
|
+
"Node's inspector (node --inspect, wrangler dev) defaults to 9229 too. " +
|
|
150
|
+
"Stop that process, or pick another port with --port <n> or SPICETIFY_CDP_PORT=<n>.");
|
|
151
|
+
}
|
|
95
152
|
export async function wsUrl(port) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
throw new Error(
|
|
101
|
-
|
|
153
|
+
const probe = await probePort(port);
|
|
154
|
+
if (probe.state === "xpui")
|
|
155
|
+
return probe.ws;
|
|
156
|
+
if (probe.state === "other")
|
|
157
|
+
throw new Error(portInUseMessage(port, probe.what));
|
|
158
|
+
if (probe.state === "spotify") {
|
|
159
|
+
throw new Error(`Spotify is listening on port ${port} but has no xpui page yet. ` +
|
|
160
|
+
"Wait for it to finish starting, and ensure `spicetify apply` has staged the v3 loader.");
|
|
102
161
|
}
|
|
103
|
-
|
|
162
|
+
if (probe.state === "unknown") {
|
|
163
|
+
throw new Error(`port ${port} answered, but not as a Spotify DevTools endpoint; retry once Spotify has started.`);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`no Spotify listening on port ${port}. Start it with --remote-debugging-port=${port}, ` +
|
|
166
|
+
"or run `spicetify-kit dev`, which starts it for you.");
|
|
167
|
+
}
|
|
168
|
+
// A freshly launched client exposes its xpui target before the loader has
|
|
169
|
+
// booted, so client-side code waits up to 10s for Spicetify.Modules.
|
|
170
|
+
const AWAIT_LOADER = `let M = globalThis.Spicetify?.Modules;
|
|
171
|
+
for (let i = 0; !M && i < 100; i++) {
|
|
172
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
173
|
+
M = globalThis.Spicetify?.Modules;
|
|
174
|
+
}`;
|
|
175
|
+
// evaluate runs an expression in the xpui page over CDP and resolves with its
|
|
176
|
+
// string value.
|
|
177
|
+
export function evaluate(port, expr, timeoutMs = 15_000) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
void wsUrl(port).then((url) => {
|
|
180
|
+
let ws;
|
|
181
|
+
try {
|
|
182
|
+
ws = new WebSocket(url);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
reject(new Error(`could not open the client websocket: ${e.message}`));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const timer = setTimeout(() => {
|
|
189
|
+
ws.close();
|
|
190
|
+
reject(new Error("client evaluation timed out"));
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
ws.addEventListener("error", (e) => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
reject(new Error(`websocket error: ${String(e.message ?? e)}`));
|
|
195
|
+
});
|
|
196
|
+
// A settled promise ignores this; it only fires for a socket the client
|
|
197
|
+
// closed before answering (for instance Spotify quitting mid-push).
|
|
198
|
+
ws.addEventListener("close", () => {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
reject(new Error("the client closed the connection before answering"));
|
|
201
|
+
});
|
|
202
|
+
ws.addEventListener("open", () => {
|
|
203
|
+
ws.send(JSON.stringify({
|
|
204
|
+
id: 1,
|
|
205
|
+
method: "Runtime.evaluate",
|
|
206
|
+
params: { expression: expr, awaitPromise: true, returnByValue: true },
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
ws.addEventListener("message", (ev) => {
|
|
210
|
+
let msg;
|
|
211
|
+
try {
|
|
212
|
+
msg = JSON.parse(String(ev.data));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (msg.id !== 1)
|
|
218
|
+
return;
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
ws.close();
|
|
221
|
+
const outcome = interpretResult(msg);
|
|
222
|
+
if ("error" in outcome)
|
|
223
|
+
reject(new Error(outcome.error));
|
|
224
|
+
else
|
|
225
|
+
resolve(outcome.value);
|
|
226
|
+
});
|
|
227
|
+
}, reject);
|
|
228
|
+
});
|
|
104
229
|
}
|
|
105
230
|
export function push(rec, id, port) {
|
|
106
231
|
// Stamp before the quota check so the stamp's own bytes are counted.
|
|
@@ -112,14 +237,18 @@ export function push(rec, id, port) {
|
|
|
112
237
|
// The whole exchange runs in the client: disable the old instance, install
|
|
113
238
|
// the fresh content, and re-enable anything the unload cascade took down.
|
|
114
239
|
const expr = `(async () => {
|
|
115
|
-
|
|
240
|
+
${AWAIT_LOADER}
|
|
116
241
|
if (!M) return JSON.stringify({ error: "loader not ready" });
|
|
117
242
|
const rec = ${JSON.stringify(rec)};
|
|
118
243
|
const id = ${JSON.stringify(id)};
|
|
119
244
|
const before = M.list().filter((m) => m.loaded).map((m) => m.identifier);
|
|
120
245
|
const hadPrevious = before.includes(id);
|
|
121
|
-
await M.
|
|
122
|
-
|
|
246
|
+
const installed = await M.installLocal(id, rec);
|
|
247
|
+
// installLocal keeps a module the client has marked disabled off. Pushing
|
|
248
|
+
// it is an explicit request to run it, so load it for this session;
|
|
249
|
+
// reload is transient, so the persisted choice still applies after removal.
|
|
250
|
+
const reenabled = installed?.disabled === true;
|
|
251
|
+
if (reenabled) await (M.reload ?? M.enable)(id);
|
|
123
252
|
// Re-enabling a theme the loader just unloaded would fight the
|
|
124
253
|
// single-active-theme invariant and knock the pushed theme back off.
|
|
125
254
|
const pushedIsTheme = (rec.metadata.tags ?? []).includes("theme");
|
|
@@ -136,40 +265,54 @@ export function push(rec, id, port) {
|
|
|
136
265
|
failed: M.report?.failed?.[id] ?? null,
|
|
137
266
|
stamp: ${stamped ? '(stampLive ? "live" : "stale")' : '"unstamped"'},
|
|
138
267
|
hadPrevious,
|
|
268
|
+
reenabled,
|
|
139
269
|
});
|
|
140
270
|
})()`;
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
271
|
+
return evaluate(port, expr);
|
|
272
|
+
}
|
|
273
|
+
// removeLocal drops a hot-pushed override so the client falls back to the
|
|
274
|
+
// staged or store-installed copy.
|
|
275
|
+
export async function removeLocal(id, port) {
|
|
276
|
+
const raw = await evaluate(port, `(async () => {
|
|
277
|
+
${AWAIT_LOADER}
|
|
278
|
+
if (!M?.removeLocal) return JSON.stringify({ error: "loader not ready" });
|
|
279
|
+
const id = ${JSON.stringify(id)};
|
|
280
|
+
const had = M.list().some((m) => m.identifier === id && m.local) ||
|
|
281
|
+
(M.listLocal?.() ?? []).some((r) => r?.metadata?.identifier === id);
|
|
282
|
+
const out = await M.removeLocal(id);
|
|
283
|
+
return JSON.stringify({ had, out: out ?? null });
|
|
284
|
+
})()`);
|
|
285
|
+
let parsed;
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(raw);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
parsed = null;
|
|
291
|
+
}
|
|
292
|
+
if (isRecord(parsed) && parsed.error) {
|
|
293
|
+
throw new Error("the v3 loader is not staged in this client (Spicetify.Modules is absent)");
|
|
294
|
+
}
|
|
295
|
+
if (!isRecord(parsed) || typeof parsed.had !== "boolean") {
|
|
296
|
+
throw new Error(`unexpected removeLocal result: ${raw.slice(0, 200)}`);
|
|
297
|
+
}
|
|
298
|
+
const out = isRecord(parsed.out) ? parsed.out : {};
|
|
299
|
+
if (out.requiresRestart === true)
|
|
300
|
+
return { kind: "requires-restart" };
|
|
301
|
+
if (typeof out.revertedTo === "string")
|
|
302
|
+
return { kind: "reverted", version: out.revertedTo };
|
|
303
|
+
return parsed.had ? { kind: "removed" } : { kind: "none" };
|
|
304
|
+
}
|
|
305
|
+
export function formatRemoveOutcome(id, outcome) {
|
|
306
|
+
switch (outcome.kind) {
|
|
307
|
+
case "none":
|
|
308
|
+
return `${id} has no local override to remove`;
|
|
309
|
+
case "removed":
|
|
310
|
+
return `removed the local override of ${id} (no staged copy, so it is no longer installed)`;
|
|
311
|
+
case "reverted":
|
|
312
|
+
return `removed the local override of ${id}; the client is back on the installed ${outcome.version}`;
|
|
313
|
+
case "requires-restart":
|
|
314
|
+
return `removed the local override of ${id}; restart Spotify to finish reverting it`;
|
|
315
|
+
}
|
|
173
316
|
}
|
|
174
317
|
// Turn the raw client-side push result into an honest, actionable line.
|
|
175
318
|
export function formatPushResult(raw) {
|
|
@@ -199,9 +342,9 @@ export function formatPushResult(raw) {
|
|
|
199
342
|
"Restart the client (or removeLocal, then push again) before trusting any verification.",
|
|
200
343
|
};
|
|
201
344
|
}
|
|
202
|
-
const remount = parsed.hadPrevious
|
|
345
|
+
const remount = (parsed.hadPrevious
|
|
203
346
|
? " — UI mounted before the push may still be the old build; re-navigate to its surface to remount"
|
|
204
|
-
: "";
|
|
347
|
+
: "") + (parsed.reenabled ? " (it was disabled in the client; the push turned it back on)" : "");
|
|
205
348
|
if (parsed.stamp === "live")
|
|
206
349
|
return { ok: true, message: `loaded, pushed build verified executing${remount}` };
|
|
207
350
|
// css-only records carry no executable entry to stamp.
|
package/dist/remove.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* remove - drop a hot-pushed local override from a running client, so it
|
|
7
|
+
* falls back to the staged or store-installed copy. Takes a module directory
|
|
8
|
+
* (its metadata.json names the id) or the id itself.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { flagValue } from "./args.js";
|
|
13
|
+
import { formatRemoveOutcome, removeLocal, resolvePort } from "./push.js";
|
|
14
|
+
const USAGE = "spicetify-kit remove <module-dir|id> [--port 9229]";
|
|
15
|
+
// moduleId reads the id from <target>/metadata.json when target is a module
|
|
16
|
+
// directory, and otherwise treats target as the id.
|
|
17
|
+
export function moduleId(target, cwd) {
|
|
18
|
+
const meta = path.join(path.resolve(cwd, target), "metadata.json");
|
|
19
|
+
if (!existsSync(meta))
|
|
20
|
+
return target;
|
|
21
|
+
const name = JSON.parse(readFileSync(meta, "utf8")).name;
|
|
22
|
+
if (typeof name !== "string" || !name)
|
|
23
|
+
throw new Error(`${meta} has no "name" to identify the module by`);
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
export async function runRemove(argv, cwd = process.cwd()) {
|
|
27
|
+
const target = argv.find((a, i) => !a.startsWith("--") && argv[i - 1] !== "--port");
|
|
28
|
+
if (!target || argv.includes("--help") || argv.includes("-h")) {
|
|
29
|
+
console.log(USAGE);
|
|
30
|
+
if (!target)
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const port = resolvePort(flagValue(argv, "port"));
|
|
35
|
+
const id = moduleId(target, cwd);
|
|
36
|
+
console.log(`[remove] ${formatRemoveOutcome(id, await removeLocal(id, port))}`);
|
|
37
|
+
}
|
package/dist/stdlib-boundary.js
CHANGED
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
export const STDLIB_BOUNDARY_WARNING_RULES = ["ambient-client", "client-dom", "direct-map"];
|
|
8
8
|
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
|
|
9
9
|
const STYLE_EXTENSIONS = new Set([".css", ".scss"]);
|
|
10
|
-
const PUBLIC_STDLIB_IMPORT = /^\/modules\/stdlib\/(?:mod\.(?:js|ts)|lib\/primitives(?:-classes|-vanilla)?\.(?:js|ts|tsx))$/;
|
|
10
|
+
const PUBLIC_STDLIB_IMPORT = /^\/modules\/stdlib\/(?:(?:mod|query)\.(?:js|ts)|lib\/primitives(?:-classes|-vanilla)?\.(?:js|ts|tsx))$/;
|
|
11
11
|
const CLIENT_DOM_PATTERN = /(?:^|[^A-Za-z0-9_-])(?:Root__|main-[A-Za-z]|player-controls__|playback-progressbar|npv-)|\[data-testid(?:=|\])/;
|
|
12
12
|
const posix = (value) => value.split(path.sep).join("/");
|
|
13
13
|
const lineAt = (text, index) => text.slice(0, index).split("\n").length;
|
|
@@ -98,7 +98,7 @@ export function inspectStdlibBoundaryFile(file, text) {
|
|
|
98
98
|
file: normalizedFile,
|
|
99
99
|
line: lineAt(source, match.index),
|
|
100
100
|
rule: "private-stdlib-import",
|
|
101
|
-
detail: `${match[1]} is private; import through /modules/stdlib/mod.js or the primitives kit`,
|
|
101
|
+
detail: `${match[1]} is private; import through /modules/stdlib/mod.js, query.js, or the primitives kit`,
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
const ambient = firstMatch(source, /\bSpicetify\s*(?:\?\.|\.)|\b(?:globalThis|window)\s*\.\s*Spicetify\b/);
|
package/dist/version.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// Kept in lockstep with package.json by release-please. Scaffolds must depend
|
|
6
6
|
// on the kit version that generated them, especially across pre-1.0 minors.
|
|
7
|
-
export const KIT_VERSION = "0.
|
|
7
|
+
export const KIT_VERSION = "0.4.0"; // x-release-please-version
|
|
8
8
|
export const KIT_DEPENDENCY_RANGE = `^${KIT_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spicetify/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Developer kit for spicetify v3 modules: scaffold, build, hot-push, and pack",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://github.com/spicetify/modules#readme",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"./from-theme": "./dist/from-theme.js",
|
|
26
26
|
"./check": "./dist/check.js",
|
|
27
27
|
"./vault": "./dist/vault.js",
|
|
28
|
-
"./install": "./dist/install.js"
|
|
28
|
+
"./install": "./dist/install.js",
|
|
29
|
+
"./remove": "./dist/remove.js"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"bin",
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
42
|
"spicetify": {
|
|
42
|
-
"stdlibVersion": "1.
|
|
43
|
+
"stdlibVersion": "1.13.0"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
51
|
"rolldown": "^1.2.0",
|
|
51
|
-
"sass-embedded": "^1.80.0"
|
|
52
|
+
"sass-embedded": "^1.80.0",
|
|
53
|
+
"@tanstack/query-core": "5.103.0"
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": {
|
|
3
|
+
"topbar": {
|
|
4
|
+
"wrapper": "bQetA_KiP9n0DQVcGa7m",
|
|
5
|
+
"left": {
|
|
6
|
+
"button": {
|
|
7
|
+
"wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium",
|
|
8
|
+
"icon": {
|
|
9
|
+
"wrapper": "e-10860-icon e-10860-icon--auto-mirror"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"button_t": {
|
|
13
|
+
"wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"right": {
|
|
17
|
+
"button": {
|
|
18
|
+
"wrapper": "e-10860-legacy-button e-10860-button-tertiary--condensed-all"
|
|
19
|
+
},
|
|
20
|
+
"button_t": {
|
|
21
|
+
"wrapper": "nR3ekhH3c1IU7dX9smcR"
|
|
22
|
+
},
|
|
23
|
+
"upgrade_button": {
|
|
24
|
+
"wrapper": "e-10860-legacy-button e-10860-legacy-button-primary"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"playbar": {
|
|
29
|
+
"widget": {},
|
|
30
|
+
"controls": {},
|
|
31
|
+
"buttons": {
|
|
32
|
+
"button": {
|
|
33
|
+
"wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-small",
|
|
34
|
+
"wrapper__active": "e-10860-legacy-button e-10860-legacy-button-primary",
|
|
35
|
+
"wrapper__indicator": "e-10860-button-primary__inner"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"navbar": {
|
|
40
|
+
"link": {
|
|
41
|
+
"wrapper": "xlH6FJnBzFT4l3TgUBre",
|
|
42
|
+
"container": "xlH6FJnBzFT4l3TgUBre",
|
|
43
|
+
"container__active": "ajKYUZPcNmFVzcAzzlwe"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"global_navbar": {},
|
|
47
|
+
"sidebar": {},
|
|
48
|
+
"panel": {}
|
|
49
|
+
},
|
|
50
|
+
"tracklist": {
|
|
51
|
+
"column_header": "e-10860-legacy-list-row__header"
|
|
52
|
+
},
|
|
53
|
+
"search_chips": {
|
|
54
|
+
"wrapper_wrapper": "e-10860-chip-group",
|
|
55
|
+
"wrapper": "e-10860-chip-group__component",
|
|
56
|
+
"container": "e-10860-chip-group__component",
|
|
57
|
+
"chip": "e-10860-legacy-chip"
|
|
58
|
+
},
|
|
59
|
+
"modal": {
|
|
60
|
+
"widget_generator": {
|
|
61
|
+
"container": "_XeAt3IgtZQepOYqU2g4",
|
|
62
|
+
"header": {
|
|
63
|
+
"container": "mnU1hqlaF5JIcrTxLnZF",
|
|
64
|
+
"close": "AslPcD0TRZBBGZofe58g"
|
|
65
|
+
},
|
|
66
|
+
"content": {
|
|
67
|
+
"container": "Xu_Z4l6TIOF1f5kDB9dt"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"track_credits": {
|
|
71
|
+
"container": "gtXQasDkhtg93wGk6yNG",
|
|
72
|
+
"header": {
|
|
73
|
+
"container": "nQ_c7yoZ2XOD6fWtmv18",
|
|
74
|
+
"close": "CaHvWv36mAu4x5QTKlb1"
|
|
75
|
+
},
|
|
76
|
+
"content": {
|
|
77
|
+
"container": "wYQNBdH2rNMGe_B4oumf"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"settings": {
|
|
82
|
+
"header": {
|
|
83
|
+
"container": "Hwax_pfCzrntLhnYXiqL"
|
|
84
|
+
},
|
|
85
|
+
"section": {
|
|
86
|
+
"container": "HdwEUcX8xijMdU7doYPt"
|
|
87
|
+
},
|
|
88
|
+
"button": {
|
|
89
|
+
"wrapper": "e-10860-legacy-button e-10860-legacy-button--small e-10860-legacy-button-secondary"
|
|
90
|
+
},
|
|
91
|
+
"text_input": "e-10860-form-input e-10860-form-control encore-text-body-medium"
|
|
92
|
+
},
|
|
93
|
+
"scrollable_text": {
|
|
94
|
+
"container": "e-10860-legacy-box e-10860-legacy-box--naked",
|
|
95
|
+
"wrapper": "e-10860-legacy-box"
|
|
96
|
+
},
|
|
97
|
+
"sort_box": {
|
|
98
|
+
"list": {
|
|
99
|
+
"button": "MYWr4UY4pKAAscUXvqrB"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"search_box": {
|
|
103
|
+
"container": "mlx0CgXAnGz0dADVsCdS",
|
|
104
|
+
"expand_button": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium aslJk0YypwKvyKXaBJcG"
|
|
105
|
+
},
|
|
106
|
+
"context_menu": {
|
|
107
|
+
"menu_item": "fmmygDM45MTS7I35OyLh"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 spicetify
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { QueryClient } from "@tanstack/query-core";
|
|
7
|
+
import type { ModuleRuntimeContext } from "./mod.ts";
|
|
8
|
+
|
|
9
|
+
export { QueryClient, QueryObserver, isCancelledError } from "@tanstack/query-core";
|
|
10
|
+
export type { QueryKey, QueryFunctionContext } from "@tanstack/query-core";
|
|
11
|
+
|
|
12
|
+
/** A cache owned by one module load; unloading cancels queries and discards data. */
|
|
13
|
+
export function createModuleQueryClient(ctx: Pick<ModuleRuntimeContext, "defer">): QueryClient {
|
|
14
|
+
const client = new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
staleTime: 5 * 60 * 1000,
|
|
18
|
+
gcTime: 10 * 60 * 1000,
|
|
19
|
+
retry: false,
|
|
20
|
+
refetchOnWindowFocus: false,
|
|
21
|
+
refetchOnReconnect: false,
|
|
22
|
+
},
|
|
23
|
+
mutations: { retry: false },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
ctx.defer(async () => {
|
|
27
|
+
await client.cancelQueries();
|
|
28
|
+
client.clear();
|
|
29
|
+
});
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
@@ -30,18 +30,106 @@ export interface DaemonCapabilities {
|
|
|
30
30
|
apply(): Promise<unknown>;
|
|
31
31
|
blockUpdates(): Promise<unknown>;
|
|
32
32
|
unblockUpdates(): Promise<unknown>;
|
|
33
|
+
updateAndApplySupported?(): Promise<boolean | null>;
|
|
34
|
+
updateAndApply?: UpdateAndApplyCapability;
|
|
35
|
+
managedSpotify?: ManagedSpotifyCapability;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ManagedSpotifyJob =
|
|
39
|
+
| { kind: "idle" }
|
|
40
|
+
| { kind: "running"; jobId: string; phase: "checking" | "downloading" | "preparing" | "activating" }
|
|
41
|
+
| { kind: "complete"; jobId: string }
|
|
42
|
+
| { kind: "failed"; jobId: string; message: string };
|
|
43
|
+
|
|
44
|
+
export type ManagedSpotifyInstallation =
|
|
45
|
+
| { kind: "external" }
|
|
46
|
+
| { kind: "unavailable"; message: string }
|
|
47
|
+
| { kind: "managed"; version: string; channel: "stable" | "testing"; nativeBlocked: boolean | null };
|
|
48
|
+
|
|
49
|
+
export type ManagedSpotifyAvailability =
|
|
50
|
+
| { kind: "current"; version: string }
|
|
51
|
+
| { kind: "ready"; version: string }
|
|
52
|
+
| { kind: "unavailable"; version: string; message: string };
|
|
53
|
+
|
|
54
|
+
export interface ManagedSpotifySnapshot {
|
|
55
|
+
installation: ManagedSpotifyInstallation;
|
|
56
|
+
job: ManagedSpotifyJob;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ManagedSpotifyCapability {
|
|
60
|
+
status(): Promise<ManagedSpotifySnapshot | null>;
|
|
61
|
+
check(): Promise<ManagedSpotifyAvailability>;
|
|
62
|
+
update(): Promise<UpdateAdmission>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type UpdateFailureCode =
|
|
66
|
+
| "unsupported-platform"
|
|
67
|
+
| "unsupported-target"
|
|
68
|
+
| "update-unavailable"
|
|
69
|
+
| "renderer-timeout"
|
|
70
|
+
| "spotify-update-failed"
|
|
71
|
+
| "apply-failed";
|
|
72
|
+
|
|
73
|
+
export type UpdateAndApplyStatus =
|
|
74
|
+
| { kind: "idle" }
|
|
75
|
+
| { kind: "accepted"; jobId: string; fromVersion: string }
|
|
76
|
+
| { kind: "waiting-for-update"; jobId: string; fromVersion: string }
|
|
77
|
+
| { kind: "downloading"; jobId: string; targetVersion: string }
|
|
78
|
+
| { kind: "installing-spotify"; jobId: string; targetVersion: string }
|
|
79
|
+
| { kind: "applying-spicetify"; jobId: string; targetVersion: string }
|
|
80
|
+
| { kind: "securing"; jobId: string; targetVersion?: string; message?: string; manualRecovery?: boolean }
|
|
81
|
+
| { kind: "complete"; jobId: string; fromVersion: string; toVersion: string }
|
|
82
|
+
| { kind: "failed-safe"; jobId: string; code: UpdateFailureCode; message: string };
|
|
83
|
+
|
|
84
|
+
export interface UpdateAdmission {
|
|
85
|
+
jobId: string;
|
|
86
|
+
disposition: "accepted" | "joined";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UpdateAndApplyCapability {
|
|
90
|
+
(): Promise<UpdateAdmission>;
|
|
91
|
+
observe(listener: (status: UpdateAndApplyStatus) => void): () => void;
|
|
33
92
|
}
|
|
34
93
|
|
|
35
94
|
export interface SnackbarCapabilities {
|
|
36
95
|
enqueueSnackbar(message: string, options?: { variant?: string }): unknown;
|
|
37
96
|
}
|
|
38
97
|
|
|
98
|
+
type CosmosBody = Record<string, unknown>;
|
|
99
|
+
type CosmosHeaders = Spicetify.CosmosAsync.Headers;
|
|
100
|
+
type CosmosMethod = Spicetify.CosmosAsync.Method;
|
|
101
|
+
type CosmosResponse = Omit<Spicetify.CosmosAsync.Response, "body"> & { body: unknown };
|
|
102
|
+
|
|
103
|
+
export interface CosmosCapabilities {
|
|
104
|
+
head(url: string, headers?: CosmosHeaders): Promise<CosmosHeaders>;
|
|
105
|
+
get(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
|
|
106
|
+
post(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
|
|
107
|
+
put(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
|
|
108
|
+
del(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
|
|
109
|
+
patch(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
|
|
110
|
+
sub(
|
|
111
|
+
url: string,
|
|
112
|
+
callback: (body: unknown) => void,
|
|
113
|
+
onError?: (error: unknown) => void,
|
|
114
|
+
body?: CosmosBody,
|
|
115
|
+
headers?: CosmosHeaders,
|
|
116
|
+
): Promise<unknown>;
|
|
117
|
+
postSub(
|
|
118
|
+
url: string,
|
|
119
|
+
body: CosmosBody | null,
|
|
120
|
+
callback: (body: unknown) => void,
|
|
121
|
+
onError?: (error: unknown) => void,
|
|
122
|
+
): Promise<unknown>;
|
|
123
|
+
request(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
|
|
124
|
+
resolve(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
|
|
125
|
+
}
|
|
126
|
+
|
|
39
127
|
export interface ClientCapabilities {
|
|
40
128
|
readonly player: typeof Spicetify.Player;
|
|
41
129
|
readonly platform: typeof Spicetify.Platform;
|
|
42
130
|
readonly storage: typeof Spicetify.LocalStorage;
|
|
43
131
|
readonly uri: typeof Spicetify.URI;
|
|
44
|
-
readonly cosmos:
|
|
132
|
+
readonly cosmos: CosmosCapabilities;
|
|
45
133
|
readonly corsProxy: typeof Spicetify.CORSProxy;
|
|
46
134
|
readonly graphQL: typeof Spicetify.GraphQL;
|
|
47
135
|
readonly locale: typeof Spicetify.Locale;
|
|
@@ -21,6 +21,15 @@ function createGlobalThisShadow() {
|
|
|
21
21
|
|
|
22
22
|
type CosmosRequest = { uri: string; method: string; body: string };
|
|
23
23
|
|
|
24
|
+
declare global {
|
|
25
|
+
var sendCosmosRequest: (request: {
|
|
26
|
+
request: string;
|
|
27
|
+
persistent?: boolean;
|
|
28
|
+
onSuccess?: (response: string) => void;
|
|
29
|
+
onFailure?: (error: unknown) => void;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
const CosmosRequestSubject = new Subject<CosmosRequest>();
|
|
25
34
|
export const UpdateTitlebarSubject = new BehaviorSubject<number>(-1);
|
|
26
35
|
|
|
@@ -41,7 +41,7 @@ const playerListener = ({ data: state }: any) => {
|
|
|
41
41
|
cachedState = state;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const historyListener = (location) => EventBus.History.updated.next(location);
|
|
44
|
+
const historyListener = (location: unknown) => EventBus.History.updated.next(location);
|
|
45
45
|
|
|
46
46
|
const updateTitlebarListener = (height: number) => EventBus.ControlMessage.titlebar_updated.next(height);
|
|
47
47
|
|