@lox-audioserver/node-librespot 0.3.2

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/Cargo.toml ADDED
@@ -0,0 +1,32 @@
1
+ [package]
2
+ name = "librespot-addon"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Rust-based librespot binding for Node via N-API."
6
+ license = "Apache-2.0"
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ napi = { version = "2", features = ["tokio_rt"] }
13
+ napi-derive = "2"
14
+ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
15
+ futures = "0.3"
16
+ bytes = "1"
17
+ bytemuck = { version = "1", features = ["extern_crate_alloc"] }
18
+ librespot-core = { path = "./librespot-dev/core", default-features = false, features = [
19
+ "rustls-tls-native-roots",
20
+ ] }
21
+ librespot-audio = { path = "./librespot-dev/audio", default-features = false, features = [
22
+ "rustls-tls-native-roots",
23
+ ] }
24
+ librespot-playback = { path = "./librespot-dev/playback", default-features = false }
25
+ librespot-discovery = { path = "./librespot-dev/discovery", default-features = false, features = [
26
+ "with-libmdns",
27
+ ] }
28
+ librespot-connect = { path = "./librespot-dev/connect", default-features = false }
29
+ librespot-metadata = { path = "./librespot-dev/metadata", default-features = false }
30
+ serde_json = "1"
31
+ tokio-stream = "0.1"
32
+ log = { version = "0.4", features = ["std"] }
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # node-librespot
2
+
3
+ Native Node.js bindings for [librespot](https://github.com/librespot-org/librespot) (Spotify Connect) using N-API. Streams PCM directly to JavaScript without spawning the librespot binary.
4
+
5
+ Used by [lox-audioserver](https://github.com/rudyberends/lox-audioserver) to handle spotify traffic, but can be used by other software.
6
+
7
+ ## Features
8
+ - Stream a Spotify track/episode to PCM buffers (`streamTrack`) using a Web API **access token**.
9
+ - Host a Spotify Connect endpoint with a Web API access token + your own Spotify app client id (`startConnectDeviceWithToken`).
10
+ - No disk cache required; everything stays in-memory.
11
+ - Starts Connect hosts at max volume (volume control stays on the consumer side).
12
+ - `startConnectDevice` (credential-blob based) is deprecated and will error.
13
+
14
+ ## Install
15
+ ```bash
16
+ npm install
17
+ ```
18
+
19
+ ## Build
20
+ Requires Rust (stable) and `@napi-rs/cli` (installed via devDependencies).
21
+ ```bash
22
+ npm run build # release build
23
+ # or
24
+ npm run build:debug # debug build
25
+ # or (for publishing) generate host prebuild + JS/types
26
+ npm run build:prebuild
27
+ ```
28
+
29
+ Runtime will load `librespot_addon.node` from `prebuilds/<platform-arch>/` when available, otherwise it falls back to `LOX_LIBRESPOT_ADDON_PATH` or the locally compiled `dist/librespot_addon.node`.
30
+
31
+ ## Usage
32
+ ```ts
33
+ import { createSession, setLogLevel } from 'node-librespot';
34
+
35
+ setLogLevel('debug'); // off|error|warn|info|debug|trace
36
+
37
+ const session = await createSession({
38
+ accessToken, // user Web API access token (PKCE/Authorization Code)
39
+ clientId, // your Spotify app client id
40
+ deviceName: 'lox-zone-1',
41
+ });
42
+
43
+ const handle = session.streamTrack(
44
+ { uri: 'spotify:track:...', bitrate: 320 },
45
+ (chunk) => {
46
+ // chunk is a Buffer with PCM s16le frames (44.1kHz stereo)
47
+ },
48
+ (event) => {
49
+ // optional events: playing/paused/loading/stopped/end_of_track/error/health/metric
50
+ // error codes include: audio_key_error, no_pcm, end_of_track, pcm_missing, pcm_stalled, unavailable
51
+ // metric names include: first_pcm_ms, buffer_stall_ms, decode_error
52
+ },
53
+ (log) => {
54
+ // optional logging callback { level, message, scope }
55
+ },
56
+ );
57
+
58
+ // Host a Connect device with the same token
59
+ import { startConnectDeviceWithToken } from 'node-librespot';
60
+
61
+ const host = await startConnectDeviceWithToken(
62
+ accessToken,
63
+ clientId,
64
+ 'Lox-AudioServer', // publish name
65
+ 'lox-zone-1', // device id
66
+ (chunk) => { /* PCM s16le frames */ },
67
+ (event) => { /* playing/paused/loading/end_of_track/error/health/metric with metadata */ },
68
+ (log) => { /* optional log callback */ },
69
+ );
70
+
71
+ host.play();
72
+ host.pause();
73
+ host.next();
74
+ host.prev();
75
+ host.stop();
76
+ host.shutdown(); // alias for stop()
77
+ host.close(); // alias for stop()
78
+
79
+ // Or pull decrypted audio file bytes (Ogg/MP3) without decoding:
80
+ const download = await downloadTrack(
81
+ { uri: 'spotify:track:...', bitrate: 320 },
82
+ (chunk) => {
83
+ // chunk is a Buffer with decrypted compressed audio data
84
+ },
85
+ (log) => {
86
+ // optional logging callback { level, message, scope }
87
+ },
88
+ );
89
+ // download.stop() to cancel
90
+ // Promise rejects on initial errors (invalid URI, unavailable track, key/file fetch failure).
91
+
92
+ handle.stop();
93
+ ```
94
+
95
+ ## Authentication
96
+ - Only Web API access tokens are supported (bring your own PKCE/Authorization Code flow).
97
+ - Supply your app’s client id via `clientId` or `LOX_LIBRESPOT_CLIENT_ID` env var.
98
+ - `startConnectDevice` is removed; use `startConnectDeviceWithToken` instead.
99
+
100
+ If the token is invalid/expired, playback/Connect hosting will fail; refresh the token in your host app and retry.
101
+
102
+ ## Development / publish
103
+ - CI: `.github/workflows/prebuild.yml` runs on release creation, building prebuild artifacts for Linux x64 (glibc), Linux arm64 (glibc), and macOS arm64 before publishing to npm (requires `NPM_TOKEN` secret).
104
+ - Local publish flow: `npm run build:prebuild` to generate host prebuild + JS/types, then `npm publish`.
105
+ - Install from source: `npm install` will try to use an included prebuild; if none matches the host it will build from source automatically (set `LOX_LIBRESPOT_SKIP_BUILD=1` to skip).
@@ -0,0 +1,19 @@
1
+ import type { ConnectHandle, ConnectEvent, CreateSessionOpts, CredentialsResult, LibrespotSession, LogEvent, DownloadTrackOpts, DownloadHandle } from './types';
2
+ declare const native: {
3
+ createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
4
+ setLogLevel(level: string): void;
5
+ loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
6
+ downloadTrack(opts: DownloadTrackOpts, onChunk: (chunk: Buffer) => void, onLog?: (event: LogEvent) => void): Promise<DownloadHandle>;
7
+ startZeroconfLogin(deviceId: string, name?: string | null, timeoutMs?: number | null): Promise<CredentialsResult>;
8
+ startConnectDevice(credentialsPath: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
9
+ startConnectDeviceWithToken(accessToken: string, clientId: string | undefined, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
10
+ };
11
+ export declare function createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
12
+ export declare function loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
13
+ export declare function startZeroconfLogin(deviceId: string, name?: string, timeoutMs?: number): Promise<CredentialsResult>;
14
+ export declare function startConnectDevice(credentialsPath: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
15
+ export declare function startConnectDeviceWithToken(accessToken: string, clientId: string | undefined, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
16
+ export declare function setLogLevel(level: string): void;
17
+ export declare function downloadTrack(opts: DownloadTrackOpts, onChunk: (chunk: Buffer) => void, onLog?: (event: LogEvent) => void): Promise<DownloadHandle>;
18
+ export { native };
19
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.native = void 0;
21
+ exports.createSession = createSession;
22
+ exports.loginWithAccessToken = loginWithAccessToken;
23
+ exports.startZeroconfLogin = startZeroconfLogin;
24
+ exports.startConnectDevice = startConnectDevice;
25
+ exports.startConnectDeviceWithToken = startConnectDeviceWithToken;
26
+ exports.setLogLevel = setLogLevel;
27
+ exports.downloadTrack = downloadTrack;
28
+ const node_fs_1 = __importDefault(require("node:fs"));
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ function detectLibc() {
31
+ const glibcVersionRuntime =
32
+ // @ts-expect-error
33
+ process.report?.getReport?.()?.header?.glibcVersionRuntime;
34
+ return glibcVersionRuntime ? 'gnu' : 'musl';
35
+ }
36
+ function platformArchABI() {
37
+ const { platform, arch } = process;
38
+ if (platform === 'linux') {
39
+ return `linux-${arch}-${detectLibc()}`;
40
+ }
41
+ if (platform === 'darwin') {
42
+ return `darwin-${arch}`;
43
+ }
44
+ if (platform === 'win32') {
45
+ return `win32-${arch}-msvc`;
46
+ }
47
+ throw new Error(`Unsupported platform ${platform}-${arch}`);
48
+ }
49
+ function resolveNativeBinding() {
50
+ const override = process.env.LOX_LIBRESPOT_ADDON_PATH;
51
+ if (override && node_fs_1.default.existsSync(override)) {
52
+ return override;
53
+ }
54
+ const prebuiltPath = node_path_1.default.join(__dirname, '..', 'prebuilds', platformArchABI(), 'librespot_addon.node');
55
+ if (node_fs_1.default.existsSync(prebuiltPath)) {
56
+ return prebuiltPath;
57
+ }
58
+ const localBuildPath = node_path_1.default.join(__dirname, 'librespot_addon.node');
59
+ if (node_fs_1.default.existsSync(localBuildPath)) {
60
+ return localBuildPath;
61
+ }
62
+ throw new Error(`librespot_addon.node not found for ${platformArchABI()}. ` +
63
+ 'Install a prebuilt binary, build locally with "npm run build", ' +
64
+ 'or point LOX_LIBRESPOT_ADDON_PATH to the compiled addon.');
65
+ }
66
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
67
+ const native = require(resolveNativeBinding());
68
+ exports.native = native;
69
+ function wrapStreamHandle(handle) {
70
+ return {
71
+ stop: () => handle.stop(),
72
+ get sampleRate() {
73
+ return handle.sampleRate ?? handle.sample_rate ?? handle.sampleRate;
74
+ },
75
+ get channels() {
76
+ return handle.channels ?? handle.channels;
77
+ },
78
+ };
79
+ }
80
+ function wrapSession(session) {
81
+ return {
82
+ streamTrack: (opts, onChunk, onEvent, onLog) => {
83
+ const nativeOpts = {
84
+ uri: opts.uri,
85
+ startPositionMs: opts.startPositionMs ?? opts.start_position_ms,
86
+ bitrate: opts.bitrate,
87
+ output: opts.output,
88
+ emitEvents: opts.emitEvents ?? opts.emit_events,
89
+ };
90
+ const handle = session.streamTrack(nativeOpts, onChunk, onEvent, onLog);
91
+ return wrapStreamHandle(handle);
92
+ },
93
+ close: () => session.close(),
94
+ };
95
+ }
96
+ function createSession(opts) {
97
+ const nativeOpts = {
98
+ accessToken: opts.accessToken ?? opts.access_token,
99
+ clientId: opts.clientId ?? opts.client_id,
100
+ deviceName: opts.deviceName ?? opts.device_name,
101
+ };
102
+ return native.createSession(nativeOpts).then((sess) => wrapSession(sess));
103
+ }
104
+ function loginWithAccessToken(accessToken, deviceName) {
105
+ return native.loginWithAccessToken(accessToken, deviceName).then((res) => {
106
+ const credentialsJson = res.credentialsJson ?? res.credentials_json;
107
+ return {
108
+ ...res,
109
+ credentialsJson,
110
+ credentials_json: credentialsJson ?? res.credentials_json,
111
+ };
112
+ });
113
+ }
114
+ function startZeroconfLogin(deviceId, name, timeoutMs) {
115
+ return native.startZeroconfLogin(deviceId, name, timeoutMs).then((res) => {
116
+ const credentialsJson = res.credentialsJson ?? res.credentials_json;
117
+ return {
118
+ ...res,
119
+ credentialsJson,
120
+ credentials_json: credentialsJson ?? res.credentials_json,
121
+ };
122
+ });
123
+ }
124
+ function startConnectDevice(credentialsPath, name, deviceId, onChunk, onEvent, onLog) {
125
+ // Legacy entrypoint kept for API compatibility; immediately fails.
126
+ return Promise.reject(new Error('startConnectDevice is deprecated; use startConnectDeviceWithToken(accessToken, clientId, ...)'));
127
+ }
128
+ function startConnectDeviceWithToken(accessToken, clientId, name, deviceId, onChunk, onEvent, onLog) {
129
+ return Promise.resolve(native.startConnectDeviceWithToken(accessToken, clientId, name, deviceId, onChunk, onEvent, onLog)).then((handle) => ({
130
+ stop: () => handle.stop(),
131
+ shutdown: () => handle.shutdown(),
132
+ close: () => handle.close(),
133
+ play: () => handle.play(),
134
+ pause: () => handle.pause(),
135
+ next: () => handle.next(),
136
+ prev: () => handle.prev(),
137
+ sampleRate: handle.sampleRate ?? handle.sample_rate ?? handle.sampleRate,
138
+ channels: handle.channels,
139
+ }));
140
+ }
141
+ function setLogLevel(level) {
142
+ native.setLogLevel(level);
143
+ }
144
+ function downloadTrack(opts, onChunk, onLog) {
145
+ const nativeOpts = {
146
+ uri: opts.uri,
147
+ bitrate: opts.bitrate,
148
+ };
149
+ return native.downloadTrack(nativeOpts, onChunk, onLog);
150
+ }
151
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,83 @@
1
+ /** Options used to create a librespot session. */
2
+ export interface CreateSessionOpts {
3
+ accessToken?: string;
4
+ clientId?: string;
5
+ deviceName?: string;
6
+ }
7
+ /** Options for streaming a track. */
8
+ export interface StreamTrackOpts {
9
+ uri: string;
10
+ startPositionMs?: number;
11
+ bitrate?: number;
12
+ output?: string;
13
+ emitEvents?: boolean;
14
+ }
15
+ /** Options for downloading raw (decrypted) audio bytes. */
16
+ export interface DownloadTrackOpts {
17
+ uri: string;
18
+ bitrate?: number;
19
+ }
20
+ /** Result of a credentials login flow. */
21
+ export interface CredentialsResult {
22
+ username: string;
23
+ credentialsJson: string;
24
+ credentials_json?: string;
25
+ }
26
+ /** Helper signature for OAuth-based credentials minting. */
27
+ export type LoginWithAccessToken = (accessToken: string, deviceName?: string) => Promise<CredentialsResult>;
28
+ export type LibrespotEventType = 'playing' | 'paused' | 'loading' | 'stopped' | 'end_of_track' | 'unavailable' | 'volume' | 'position_correction' | 'health' | 'error' | 'metric' | 'preloading' | 'time_to_preload' | 'play_request_id' | 'other';
29
+ export type LibrespotErrorCode = 'audio_key_error' | 'no_pcm' | 'end_of_track' | 'pcm_missing' | 'pcm_stalled' | 'pcm_ok' | 'unavailable' | 'unknown';
30
+ /** Event payload emitted by the connect host. */
31
+ export interface ConnectEvent {
32
+ type: LibrespotEventType;
33
+ deviceId?: string;
34
+ sessionId?: string;
35
+ trackId?: string;
36
+ uri?: string;
37
+ title?: string;
38
+ artist?: string;
39
+ album?: string;
40
+ durationMs?: number;
41
+ positionMs?: number;
42
+ volume?: number;
43
+ errorCode?: LibrespotErrorCode;
44
+ errorMessage?: string;
45
+ metricName?: string;
46
+ metricValueMs?: number;
47
+ metricMessage?: string;
48
+ }
49
+ /** Log payload emitted by the native module. */
50
+ export interface LogEvent {
51
+ level: string;
52
+ message: string;
53
+ scope?: string;
54
+ deviceId?: string;
55
+ sessionId?: string;
56
+ }
57
+ /** Handle returned when hosting a connect device. */
58
+ export interface ConnectHandle {
59
+ stop(): void;
60
+ shutdown(): void;
61
+ close(): void;
62
+ play(): void;
63
+ pause(): void;
64
+ next(): void;
65
+ prev(): void;
66
+ sampleRate: number;
67
+ channels: number;
68
+ }
69
+ /** Handle to a librespot session. */
70
+ export interface LibrespotSession {
71
+ streamTrack(opts: StreamTrackOpts, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): StreamHandle;
72
+ close(): Promise<void>;
73
+ }
74
+ export interface StreamHandle {
75
+ stop(): void;
76
+ sampleRate: number;
77
+ channels: number;
78
+ }
79
+ /** Handle for a raw download stream. */
80
+ export interface DownloadHandle {
81
+ stop(): void;
82
+ }
83
+ export declare function setLogLevel(level: string): void;
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /* Auto-generated from the addon shape; keep in sync with N-API exports. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@lox-audioserver/node-librespot",
3
+ "version": "0.3.2",
4
+ "description": "Node.js bindings for librespot (Spotify Connect) via N-API with prebuild support.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*",
9
+ "prebuilds/**/*",
10
+ "index.d.ts",
11
+ "scripts/**/*",
12
+ "src/**/*",
13
+ "native/**/*",
14
+ "Cargo.toml",
15
+ "Cargo.lock",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "npm run clean && npm run build:native && npm run build:types",
20
+ "build:debug": "npm run clean && napi build && mkdir -p dist && mv -f librespot_addon.node dist/ && npm run build:types",
21
+ "build:native": "napi build --release --strip && mkdir -p dist && mv -f librespot_addon.node dist/",
22
+ "build:prebuild": "node scripts/prebuild.js",
23
+ "build:types": "tsc -p tsconfig.build.json && rm -f index.d.ts",
24
+ "build:types:clean": "rm -rf dist && npm run build:types",
25
+ "clean": "rm -rf build dist prebuilds *.log *.node target index.d.ts",
26
+ "test": "node scripts/test.js",
27
+ "test:connect": "node scripts/test-connect.js",
28
+ "test:service": "node scripts/test-service.js",
29
+ "update-librespot": "bash scripts/update-librespot.sh",
30
+ "prepublishOnly": "npm run build:types",
31
+ "postinstall": "node scripts/postinstall.js"
32
+ },
33
+ "keywords": [
34
+ "spotify",
35
+ "connect",
36
+ "librespot",
37
+ "napi",
38
+ "audio"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/lox-audioserver/node-librespot"
43
+ },
44
+ "author": "Rudy Berends",
45
+ "license": "Apache-2.0",
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "devDependencies": {
50
+ "@napi-rs/cli": "^2.18.4",
51
+ "@tsconfig/node20": "^20.1.2",
52
+ "@types/node": "^22.19.3",
53
+ "typescript": "^5.4.0"
54
+ },
55
+ "dependencies": {
56
+ "node-addon-api": "^8.2.1"
57
+ },
58
+ "napi": {
59
+ "name": "librespot_addon",
60
+ "targets": [
61
+ "node"
62
+ ]
63
+ }
64
+ }
@@ -0,0 +1,59 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { spawnSync } = require('node:child_process');
4
+
5
+ const ROOT = path.resolve(__dirname, '..');
6
+ const ADDON_NAME = 'librespot_addon.node';
7
+
8
+ function detectLibc() {
9
+ const glibcVersionRuntime =
10
+ // @ts-expect-error
11
+ process.report?.getReport?.()?.header?.glibcVersionRuntime;
12
+ return glibcVersionRuntime ? 'gnu' : 'musl';
13
+ }
14
+
15
+ function platformArchABI() {
16
+ const { platform, arch } = process;
17
+ if (platform === 'linux') return `linux-${arch}-${detectLibc()}`;
18
+ if (platform === 'darwin') return `darwin-${arch}`;
19
+ if (platform === 'win32') return `win32-${arch}-msvc`;
20
+ return `${platform}-${arch}`;
21
+ }
22
+
23
+ function resolveAddonPaths() {
24
+ const prebuildPath = path.join(ROOT, 'prebuilds', platformArchABI(), ADDON_NAME);
25
+ const distPath = path.join(ROOT, 'dist', ADDON_NAME);
26
+ return { prebuildPath, distPath };
27
+ }
28
+
29
+ function addonExists() {
30
+ const { prebuildPath, distPath } = resolveAddonPaths();
31
+ return fs.existsSync(prebuildPath) || fs.existsSync(distPath);
32
+ }
33
+
34
+ function buildNative() {
35
+ const res = spawnSync('npm', ['run', 'build'], {
36
+ stdio: 'inherit',
37
+ cwd: ROOT,
38
+ env: process.env,
39
+ });
40
+ if (res.status !== 0) {
41
+ throw new Error(`npm run build failed with status ${res.status ?? 'unknown'}`);
42
+ }
43
+ }
44
+
45
+ function main() {
46
+ if (process.env.LOX_LIBRESPOT_SKIP_BUILD) {
47
+ console.warn('node-librespot: skipping native build because LOX_LIBRESPOT_SKIP_BUILD is set.');
48
+ return;
49
+ }
50
+
51
+ if (addonExists()) {
52
+ return;
53
+ }
54
+
55
+ console.info('node-librespot: no prebuild found for this platform, building from source...');
56
+ buildNative();
57
+ }
58
+
59
+ main();
@@ -0,0 +1,23 @@
1
+ const { execSync } = require('node:child_process');
2
+ const path = require('node:path');
3
+ const { preparePrebuilds } = require('./prepare-prebuilds');
4
+
5
+ const run = (cmd, env = {}) =>
6
+ execSync(cmd, {
7
+ stdio: 'inherit',
8
+ env: { ...process.env, ...env },
9
+ });
10
+
11
+ run('npm run clean');
12
+
13
+ const args = ['napi', 'build', '--release', '--platform', '--strip'];
14
+ if (process.env.USE_ZIG === '1') {
15
+ args.push('--zig');
16
+ }
17
+ if (process.env.RUST_TARGET) {
18
+ args.push('--target', process.env.RUST_TARGET);
19
+ }
20
+
21
+ run(args.join(' '));
22
+ preparePrebuilds(path.resolve(process.cwd()));
23
+ run('npm run build:types');
@@ -0,0 +1,44 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const BINARY_NAME = 'librespot_addon';
5
+
6
+ function preparePrebuilds(cwd = process.cwd()) {
7
+ const files = fs
8
+ .readdirSync(cwd)
9
+ .filter((file) => file.startsWith(`${BINARY_NAME}.`) && file.endsWith('.node'));
10
+
11
+ const moved = [];
12
+
13
+ for (const file of files) {
14
+ if (file === `${BINARY_NAME}.node`) {
15
+ continue;
16
+ }
17
+
18
+ const suffix = file.slice(BINARY_NAME.length + 1, -'.node'.length);
19
+ if (!suffix) {
20
+ continue;
21
+ }
22
+
23
+ const destDir = path.join(cwd, 'prebuilds', suffix);
24
+ fs.mkdirSync(destDir, { recursive: true });
25
+
26
+ const sourcePath = path.join(cwd, file);
27
+ const destPath = path.join(destDir, `${BINARY_NAME}.node`);
28
+ fs.renameSync(sourcePath, destPath);
29
+
30
+ moved.push(path.relative(cwd, destPath));
31
+ }
32
+
33
+ if (!moved.length) {
34
+ throw new Error(`No prebuild outputs found for ${BINARY_NAME}. Run "napi build --platform --release" first.`);
35
+ }
36
+
37
+ console.log(`Prebuilds ready:\n${moved.map((p) => ` - ${p}`).join('\n')}`);
38
+ }
39
+
40
+ if (require.main === module) {
41
+ preparePrebuilds();
42
+ }
43
+
44
+ module.exports = { preparePrebuilds };