@lox-audioserver/node-librespot 0.3.2 → 0.3.3

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.lock CHANGED
@@ -1220,6 +1220,7 @@ dependencies = [
1220
1220
  "tokio-util",
1221
1221
  "url",
1222
1222
  "uuid",
1223
+ "vergen",
1223
1224
  "vergen-gitcl",
1224
1225
  ]
1225
1226
 
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  import type { ConnectHandle, ConnectEvent, CreateSessionOpts, CredentialsResult, LibrespotSession, LogEvent, DownloadTrackOpts, DownloadHandle } from './types';
2
2
  declare const native: {
3
3
  createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
4
+ createSessionWithCredentials(credentialsPath: string, deviceName?: string | null): Promise<LibrespotSession>;
4
5
  setLogLevel(level: string): void;
5
6
  loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
6
7
  downloadTrack(opts: DownloadTrackOpts, onChunk: (chunk: Buffer) => void, onLog?: (event: LogEvent) => void): Promise<DownloadHandle>;
7
8
  startZeroconfLogin(deviceId: string, name?: string | null, timeoutMs?: number | null): Promise<CredentialsResult>;
8
9
  startConnectDevice(credentialsPath: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
10
+ startConnectDeviceWithCredentials(credentialsPath: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
9
11
  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
12
  };
11
13
  export declare function createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
14
+ export declare function createSessionWithCredentials(credentialsPathOrJson: string, deviceName?: string | null): Promise<LibrespotSession>;
12
15
  export declare function loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
13
16
  export declare function startZeroconfLogin(deviceId: string, name?: string, timeoutMs?: number): Promise<CredentialsResult>;
14
17
  export declare function startConnectDevice(credentialsPath: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
18
+ export declare function startConnectDeviceWithCredentials(credentialsPathOrJson: string, name: string, deviceId: string, onChunk: (chunk: Buffer) => void, onEvent?: (event: ConnectEvent) => void, onLog?: (event: LogEvent) => void): Promise<ConnectHandle>;
15
19
  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
20
  export declare function setLogLevel(level: string): void;
17
21
  export declare function downloadTrack(opts: DownloadTrackOpts, onChunk: (chunk: Buffer) => void, onLog?: (event: LogEvent) => void): Promise<DownloadHandle>;
package/dist/index.js CHANGED
@@ -19,9 +19,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.native = void 0;
21
21
  exports.createSession = createSession;
22
+ exports.createSessionWithCredentials = createSessionWithCredentials;
22
23
  exports.loginWithAccessToken = loginWithAccessToken;
23
24
  exports.startZeroconfLogin = startZeroconfLogin;
24
25
  exports.startConnectDevice = startConnectDevice;
26
+ exports.startConnectDeviceWithCredentials = startConnectDeviceWithCredentials;
25
27
  exports.startConnectDeviceWithToken = startConnectDeviceWithToken;
26
28
  exports.setLogLevel = setLogLevel;
27
29
  exports.downloadTrack = downloadTrack;
@@ -101,6 +103,11 @@ function createSession(opts) {
101
103
  };
102
104
  return native.createSession(nativeOpts).then((sess) => wrapSession(sess));
103
105
  }
106
+ function createSessionWithCredentials(credentialsPathOrJson, deviceName) {
107
+ return native
108
+ .createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null)
109
+ .then((sess) => wrapSession(sess));
110
+ }
104
111
  function loginWithAccessToken(accessToken, deviceName) {
105
112
  return native.loginWithAccessToken(accessToken, deviceName).then((res) => {
106
113
  const credentialsJson = res.credentialsJson ?? res.credentials_json;
@@ -125,6 +132,19 @@ function startConnectDevice(credentialsPath, name, deviceId, onChunk, onEvent, o
125
132
  // Legacy entrypoint kept for API compatibility; immediately fails.
126
133
  return Promise.reject(new Error('startConnectDevice is deprecated; use startConnectDeviceWithToken(accessToken, clientId, ...)'));
127
134
  }
135
+ function startConnectDeviceWithCredentials(credentialsPathOrJson, name, deviceId, onChunk, onEvent, onLog) {
136
+ return Promise.resolve(native.startConnectDeviceWithCredentials(credentialsPathOrJson, name, deviceId, onChunk, onEvent, onLog)).then((handle) => ({
137
+ stop: () => handle.stop(),
138
+ shutdown: () => handle.shutdown(),
139
+ close: () => handle.close(),
140
+ play: () => handle.play(),
141
+ pause: () => handle.pause(),
142
+ next: () => handle.next(),
143
+ prev: () => handle.prev(),
144
+ sampleRate: handle.sampleRate ?? handle.sample_rate ?? handle.sampleRate,
145
+ channels: handle.channels,
146
+ }));
147
+ }
128
148
  function startConnectDeviceWithToken(accessToken, clientId, name, deviceId, onChunk, onEvent, onLog) {
129
149
  return Promise.resolve(native.startConnectDeviceWithToken(accessToken, clientId, name, deviceId, onChunk, onEvent, onLog)).then((handle) => ({
130
150
  stop: () => handle.stop(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lox-audioserver/node-librespot",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Node.js bindings for librespot (Spotify Connect) via N-API with prebuild support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/lox-audioserver/node-librespot"
42
+ "url": "git+https://github.com/lox-audioserver/node-librespot.git"
43
43
  },
44
44
  "author": "Rudy Berends",
45
45
  "license": "Apache-2.0",
package/src/index.ts CHANGED
@@ -67,6 +67,10 @@ function resolveNativeBinding() {
67
67
  // eslint-disable-next-line @typescript-eslint/no-var-requires
68
68
  const native = require(resolveNativeBinding()) as {
69
69
  createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
70
+ createSessionWithCredentials(
71
+ credentialsPath: string,
72
+ deviceName?: string | null,
73
+ ): Promise<LibrespotSession>;
70
74
  setLogLevel(level: string): void;
71
75
  loginWithAccessToken(
72
76
  accessToken: string,
@@ -90,6 +94,14 @@ const native = require(resolveNativeBinding()) as {
90
94
  onEvent?: (event: ConnectEvent) => void,
91
95
  onLog?: (event: LogEvent) => void,
92
96
  ): Promise<ConnectHandle>;
97
+ startConnectDeviceWithCredentials(
98
+ credentialsPath: string,
99
+ name: string,
100
+ deviceId: string,
101
+ onChunk: (chunk: Buffer) => void,
102
+ onEvent?: (event: ConnectEvent) => void,
103
+ onLog?: (event: LogEvent) => void,
104
+ ): Promise<ConnectHandle>;
93
105
  startConnectDeviceWithToken(
94
106
  accessToken: string,
95
107
  clientId: string | undefined,
@@ -144,6 +156,15 @@ export function createSession(opts: CreateSessionOpts): Promise<LibrespotSession
144
156
  return native.createSession(nativeOpts as CreateSessionOpts).then((sess) => wrapSession(sess) as any);
145
157
  }
146
158
 
159
+ export function createSessionWithCredentials(
160
+ credentialsPathOrJson: string,
161
+ deviceName?: string | null,
162
+ ): Promise<LibrespotSession> {
163
+ return native
164
+ .createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null)
165
+ .then((sess) => wrapSession(sess) as any);
166
+ }
167
+
147
168
  export function loginWithAccessToken(
148
169
  accessToken: string,
149
170
  deviceName?: string,
@@ -187,6 +208,29 @@ export function startConnectDevice(
187
208
  );
188
209
  }
189
210
 
211
+ export function startConnectDeviceWithCredentials(
212
+ credentialsPathOrJson: string,
213
+ name: string,
214
+ deviceId: string,
215
+ onChunk: (chunk: Buffer) => void,
216
+ onEvent?: (event: ConnectEvent) => void,
217
+ onLog?: (event: LogEvent) => void,
218
+ ): Promise<ConnectHandle> {
219
+ return Promise.resolve(
220
+ native.startConnectDeviceWithCredentials(credentialsPathOrJson, name, deviceId, onChunk, onEvent, onLog),
221
+ ).then((handle: ConnectHandle & { sample_rate?: number }) => ({
222
+ stop: () => handle.stop(),
223
+ shutdown: () => handle.shutdown(),
224
+ close: () => handle.close(),
225
+ play: () => handle.play(),
226
+ pause: () => handle.pause(),
227
+ next: () => handle.next(),
228
+ prev: () => handle.prev(),
229
+ sampleRate: (handle as any).sampleRate ?? (handle as any).sample_rate ?? (handle as any).sampleRate,
230
+ channels: (handle as any).channels,
231
+ }));
232
+ }
233
+
190
234
  export function startConnectDeviceWithToken(
191
235
  accessToken: string,
192
236
  clientId: string | undefined,
package/src/lib.rs CHANGED
@@ -1505,6 +1505,58 @@ pub async fn create_session(opts: CreateSessionOpts) -> Result<LibrespotSession>
1505
1505
  })
1506
1506
  }
1507
1507
 
1508
+ /// Create a session using a reusable librespot credentials blob (JSON) or a path to credentials.json.
1509
+ ///
1510
+ /// This avoids relying on a Web API access token for streaming.
1511
+ #[napi]
1512
+ pub async fn create_session_with_credentials(
1513
+ credentials_path: String,
1514
+ device_name: Option<String>,
1515
+ ) -> Result<LibrespotSession> {
1516
+ if credentials_path.trim().is_empty() {
1517
+ return Err(Error::from_reason("credentials payload is required"));
1518
+ }
1519
+
1520
+ let credentials: Credentials = if Path::new(&credentials_path).exists() {
1521
+ let mut file =
1522
+ File::open(&credentials_path).map_err(|e| Error::from_reason(format!("{e}")))?;
1523
+ let mut buf = String::new();
1524
+ file.read_to_string(&mut buf)
1525
+ .map_err(|e| Error::from_reason(format!("{e}")))?;
1526
+ serde_json::from_str(&buf)
1527
+ .map_err(|e| Error::from_reason(format!("invalid credentials json: {e}")))?
1528
+ } else {
1529
+ serde_json::from_str(&credentials_path)
1530
+ .map_err(|e| Error::from_reason(format!("invalid credentials json: {e}")))?
1531
+ };
1532
+
1533
+ let mut session_config = SessionConfig::default();
1534
+ let mut device_id = device_name.unwrap_or_else(|| "librespot".to_string());
1535
+ if device_id.trim().is_empty() {
1536
+ device_id = "librespot".to_string();
1537
+ }
1538
+ session_config.device_id = device_id.clone();
1539
+ if let Ok(client_id_override) = std::env::var("LOX_LIBRESPOT_CLIENT_ID") {
1540
+ if !client_id_override.trim().is_empty() {
1541
+ session_config.client_id = client_id_override;
1542
+ }
1543
+ }
1544
+
1545
+ let session = Session::new(session_config, None);
1546
+ session
1547
+ .connect(credentials, false)
1548
+ .await
1549
+ .map_err(|e| Error::from_reason(format!("session connect failed: {e}")))?;
1550
+
1551
+ let player_config = PlayerConfig::default();
1552
+
1553
+ Ok(LibrespotSession {
1554
+ session,
1555
+ player_config,
1556
+ device_id,
1557
+ })
1558
+ }
1559
+
1508
1560
  /// Internal helper to start a Spotify Connect device using provided credentials.
1509
1561
  /// Accepts credentials (typically created from an OAuth access token) and is shared by the
1510
1562
  /// token-based public entrypoint.
@@ -1646,6 +1698,7 @@ fn start_connect_device_inner(
1646
1698
  name: name.clone(),
1647
1699
  device_type: DeviceType::Speaker,
1648
1700
  is_group: false,
1701
+ emit_set_queue_events: false,
1649
1702
  // Start with full volume so we rely on zone-side volume control; we do not sync Spotify volume.
1650
1703
  // Spotify volume scale is 0..65535; use max to avoid muted start.
1651
1704
  initial_volume: u16::MAX,
@@ -2130,6 +2183,32 @@ pub fn start_connect_device(
2130
2183
  ))
2131
2184
  }
2132
2185
 
2186
+ /// Start a Spotify Connect device using an existing credentials JSON blob (or a path to credentials.json).
2187
+ ///
2188
+ /// This avoids exchanging a Web API token for credentials, and allows reusing credentials minted via
2189
+ /// Zeroconf or other flows.
2190
+ #[napi]
2191
+ pub fn start_connect_device_with_credentials(
2192
+ credentials_path: String,
2193
+ name: String,
2194
+ device_id: String,
2195
+ on_chunk: JsFunction,
2196
+ on_event: Option<JsFunction>,
2197
+ on_log: Option<JsFunction>,
2198
+ ) -> Result<ConnectHandle> {
2199
+ if credentials_path.trim().is_empty() {
2200
+ return Err(Error::from_reason("credentials payload is required"));
2201
+ }
2202
+ start_connect_device_inner(
2203
+ credentials_path,
2204
+ name,
2205
+ device_id,
2206
+ on_chunk,
2207
+ on_event,
2208
+ on_log,
2209
+ )
2210
+ }
2211
+
2133
2212
  /// Start a Spotify Connect device using a Web API access token + client id (bypasses builtin login).
2134
2213
  #[napi]
2135
2214
  pub fn start_connect_device_with_token(