@lox-audioserver/node-librespot 0.3.6 → 0.4.1
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/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/librespot_addon.node +0 -0
- package/prebuilds/linux-arm64-gnu/librespot_addon.node +0 -0
- package/prebuilds/linux-x64-gnu/librespot_addon.node +0 -0
- package/src/index.ts +9 -3
- package/src/lib.rs +53 -5
- package/src/types.ts +6 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
+
createSessionWithCredentials(credentialsPath: string, deviceName?: string | null, cacheDir?: string | null, cacheSizeLimitMb?: number | null): Promise<LibrespotSession>;
|
|
5
5
|
setLogLevel(level: string): void;
|
|
6
6
|
loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
|
|
7
7
|
downloadTrack(opts: DownloadTrackOpts, onChunk: (chunk: Buffer) => void, onLog?: (event: LogEvent) => void): Promise<DownloadHandle>;
|
|
@@ -11,7 +11,7 @@ declare const native: {
|
|
|
11
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>;
|
|
12
12
|
};
|
|
13
13
|
export declare function createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
|
|
14
|
-
export declare function createSessionWithCredentials(credentialsPathOrJson: string, deviceName?: string | null): Promise<LibrespotSession>;
|
|
14
|
+
export declare function createSessionWithCredentials(credentialsPathOrJson: string, deviceName?: string | null, cacheDir?: string | null, cacheSizeLimitMb?: number | null): Promise<LibrespotSession>;
|
|
15
15
|
export declare function loginWithAccessToken(accessToken: string, deviceName?: string): Promise<CredentialsResult>;
|
|
16
16
|
export declare function startZeroconfLogin(deviceId: string, name?: string, timeoutMs?: number): Promise<CredentialsResult>;
|
|
17
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>;
|
package/dist/index.js
CHANGED
|
@@ -100,12 +100,14 @@ function createSession(opts) {
|
|
|
100
100
|
accessToken: opts.accessToken ?? opts.access_token,
|
|
101
101
|
clientId: opts.clientId ?? opts.client_id,
|
|
102
102
|
deviceName: opts.deviceName ?? opts.device_name,
|
|
103
|
+
cacheDir: opts.cacheDir,
|
|
104
|
+
cacheSizeLimitMb: opts.cacheSizeLimitMb,
|
|
103
105
|
};
|
|
104
106
|
return native.createSession(nativeOpts).then((sess) => wrapSession(sess));
|
|
105
107
|
}
|
|
106
|
-
function createSessionWithCredentials(credentialsPathOrJson, deviceName) {
|
|
108
|
+
function createSessionWithCredentials(credentialsPathOrJson, deviceName, cacheDir, cacheSizeLimitMb) {
|
|
107
109
|
return native
|
|
108
|
-
.createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null)
|
|
110
|
+
.createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null, cacheDir ?? null, cacheSizeLimitMb ?? null)
|
|
109
111
|
.then((sess) => wrapSession(sess));
|
|
110
112
|
}
|
|
111
113
|
function loginWithAccessToken(accessToken, deviceName) {
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export interface CreateSessionOpts {
|
|
|
3
3
|
accessToken?: string;
|
|
4
4
|
clientId?: string;
|
|
5
5
|
deviceName?: string;
|
|
6
|
+
/** Directory for the audio file cache. When set, decoded audio is stored so
|
|
7
|
+
* subsequent plays of the same track skip the CDN download. */
|
|
8
|
+
cacheDir?: string;
|
|
9
|
+
/** Maximum size of the audio cache in megabytes. Only meaningful when
|
|
10
|
+
* `cacheDir` is set. Omit for no limit. */
|
|
11
|
+
cacheSizeLimitMb?: number;
|
|
6
12
|
}
|
|
7
13
|
/** Options for streaming a track. */
|
|
8
14
|
export interface StreamTrackOpts {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,8 @@ const native = require(resolveNativeBinding()) as {
|
|
|
70
70
|
createSessionWithCredentials(
|
|
71
71
|
credentialsPath: string,
|
|
72
72
|
deviceName?: string | null,
|
|
73
|
+
cacheDir?: string | null,
|
|
74
|
+
cacheSizeLimitMb?: number | null,
|
|
73
75
|
): Promise<LibrespotSession>;
|
|
74
76
|
setLogLevel(level: string): void;
|
|
75
77
|
loginWithAccessToken(
|
|
@@ -148,20 +150,24 @@ function wrapSession(session: LibrespotSession) {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export function createSession(opts: CreateSessionOpts): Promise<LibrespotSession> {
|
|
151
|
-
const nativeOpts = {
|
|
153
|
+
const nativeOpts: CreateSessionOpts = {
|
|
152
154
|
accessToken: (opts as any).accessToken ?? (opts as any).access_token,
|
|
153
155
|
clientId: (opts as any).clientId ?? (opts as any).client_id,
|
|
154
156
|
deviceName: (opts as any).deviceName ?? (opts as any).device_name,
|
|
157
|
+
cacheDir: opts.cacheDir,
|
|
158
|
+
cacheSizeLimitMb: opts.cacheSizeLimitMb,
|
|
155
159
|
};
|
|
156
|
-
return native.createSession(nativeOpts
|
|
160
|
+
return native.createSession(nativeOpts).then((sess) => wrapSession(sess) as any);
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
export function createSessionWithCredentials(
|
|
160
164
|
credentialsPathOrJson: string,
|
|
161
165
|
deviceName?: string | null,
|
|
166
|
+
cacheDir?: string | null,
|
|
167
|
+
cacheSizeLimitMb?: number | null,
|
|
162
168
|
): Promise<LibrespotSession> {
|
|
163
169
|
return native
|
|
164
|
-
.createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null)
|
|
170
|
+
.createSessionWithCredentials(credentialsPathOrJson, deviceName ?? null, cacheDir ?? null, cacheSizeLimitMb ?? null)
|
|
165
171
|
.then((sess) => wrapSession(sess) as any);
|
|
166
172
|
}
|
|
167
173
|
|
package/src/lib.rs
CHANGED
|
@@ -120,6 +120,12 @@ pub struct CreateSessionOpts {
|
|
|
120
120
|
pub access_token: Option<String>,
|
|
121
121
|
pub client_id: Option<String>,
|
|
122
122
|
pub device_name: Option<String>,
|
|
123
|
+
/// Directory for the librespot audio file cache. When set, decoded audio
|
|
124
|
+
/// is written here so subsequent plays avoid a CDN round-trip.
|
|
125
|
+
pub cache_dir: Option<String>,
|
|
126
|
+
/// Maximum size of the audio cache in megabytes. Only used when
|
|
127
|
+
/// `cache_dir` is set. None means no size limit is enforced.
|
|
128
|
+
pub cache_size_limit_mb: Option<u32>,
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
/// Options for streaming a track.
|
|
@@ -507,6 +513,11 @@ impl Sink for ChannelSink {
|
|
|
507
513
|
}
|
|
508
514
|
|
|
509
515
|
fn stop(&mut self) -> SinkResult<()> {
|
|
516
|
+
// Reset pacing so the next start() (e.g. after Connect pause) is paced from now,
|
|
517
|
+
// not from the original stream_start. Without this the post-resume catch-up logic
|
|
518
|
+
// forwards minutes of decoded PCM at full speed.
|
|
519
|
+
self.start = None;
|
|
520
|
+
self.expected_elapsed = Duration::from_millis(0);
|
|
510
521
|
Ok(())
|
|
511
522
|
}
|
|
512
523
|
|
|
@@ -600,10 +611,20 @@ impl Sink for ChannelSink {
|
|
|
600
611
|
if self.tx.try_send(bytes).is_err() {
|
|
601
612
|
// Drop chunk if JS side is backpressured to avoid blocking the player thread.
|
|
602
613
|
}
|
|
603
|
-
let start = self.start.get_or_insert_with(Instant::now);
|
|
604
|
-
self.expected_elapsed += duration;
|
|
605
|
-
let target = *start + self.expected_elapsed;
|
|
606
614
|
let now = Instant::now();
|
|
615
|
+
// If wall-clock has drifted far ahead of our expected timeline (a pause that didn't
|
|
616
|
+
// route through stop()/start(), a decoder stall, etc.), rebase pacing onto `now`.
|
|
617
|
+
// Otherwise `sleep_dur` saturates to 0 for the duration of the gap and the sink
|
|
618
|
+
// dumps the catch-up backlog at full speed downstream.
|
|
619
|
+
if let Some(start) = self.start {
|
|
620
|
+
if now > start + self.expected_elapsed + Duration::from_millis(200) {
|
|
621
|
+
self.start = None;
|
|
622
|
+
self.expected_elapsed = Duration::from_millis(0);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
let start = *self.start.get_or_insert(now);
|
|
626
|
+
self.expected_elapsed += duration;
|
|
627
|
+
let target = start + self.expected_elapsed;
|
|
607
628
|
let sleep_dur = target.saturating_duration_since(now);
|
|
608
629
|
if !sleep_dur.is_zero() {
|
|
609
630
|
sleep(sleep_dur);
|
|
@@ -1515,6 +1536,29 @@ impl LibrespotSession {
|
|
|
1515
1536
|
}
|
|
1516
1537
|
}
|
|
1517
1538
|
|
|
1539
|
+
/// Build a librespot `Cache` for audio file storage.
|
|
1540
|
+
/// Returns `None` if `cache_dir` is absent, the directory cannot be created,
|
|
1541
|
+
/// or `Cache::new` fails — in which case streaming still works without cache.
|
|
1542
|
+
fn build_audio_cache(cache_dir: Option<&str>, cache_size_limit_mb: Option<u32>) -> Option<Cache> {
|
|
1543
|
+
let dir = cache_dir?;
|
|
1544
|
+
if dir.trim().is_empty() {
|
|
1545
|
+
return None;
|
|
1546
|
+
}
|
|
1547
|
+
let path = std::path::Path::new(dir);
|
|
1548
|
+
if let Err(e) = fs::create_dir_all(path) {
|
|
1549
|
+
eprintln!("[lox-librespot] could not create cache dir {dir}: {e}");
|
|
1550
|
+
return None;
|
|
1551
|
+
}
|
|
1552
|
+
let size_limit = cache_size_limit_mb.map(|mb| mb as u64 * 1024 * 1024);
|
|
1553
|
+
Cache::new(
|
|
1554
|
+
Some(path),
|
|
1555
|
+
None::<&std::path::Path>,
|
|
1556
|
+
None::<&std::path::Path>,
|
|
1557
|
+
size_limit,
|
|
1558
|
+
)
|
|
1559
|
+
.ok()
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1518
1562
|
/// Create a session using a Web API access token (client id optional via opts or env).
|
|
1519
1563
|
#[napi]
|
|
1520
1564
|
pub async fn create_session(opts: CreateSessionOpts) -> Result<LibrespotSession> {
|
|
@@ -1543,7 +1587,8 @@ pub async fn create_session(opts: CreateSessionOpts) -> Result<LibrespotSession>
|
|
|
1543
1587
|
}
|
|
1544
1588
|
}
|
|
1545
1589
|
|
|
1546
|
-
let
|
|
1590
|
+
let cache = build_audio_cache(opts.cache_dir.as_deref(), opts.cache_size_limit_mb);
|
|
1591
|
+
let session = Session::new(session_config, cache);
|
|
1547
1592
|
session
|
|
1548
1593
|
.connect(credentials, false)
|
|
1549
1594
|
.await
|
|
@@ -1565,6 +1610,8 @@ pub async fn create_session(opts: CreateSessionOpts) -> Result<LibrespotSession>
|
|
|
1565
1610
|
pub async fn create_session_with_credentials(
|
|
1566
1611
|
credentials_path: String,
|
|
1567
1612
|
device_name: Option<String>,
|
|
1613
|
+
cache_dir: Option<String>,
|
|
1614
|
+
cache_size_limit_mb: Option<u32>,
|
|
1568
1615
|
) -> Result<LibrespotSession> {
|
|
1569
1616
|
if credentials_path.trim().is_empty() {
|
|
1570
1617
|
return Err(Error::from_reason("credentials payload is required"));
|
|
@@ -1595,7 +1642,8 @@ pub async fn create_session_with_credentials(
|
|
|
1595
1642
|
}
|
|
1596
1643
|
}
|
|
1597
1644
|
|
|
1598
|
-
let
|
|
1645
|
+
let cache = build_audio_cache(cache_dir.as_deref(), cache_size_limit_mb);
|
|
1646
|
+
let session = Session::new(session_config, cache);
|
|
1599
1647
|
session
|
|
1600
1648
|
.connect(credentials, false)
|
|
1601
1649
|
.await
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface CreateSessionOpts {
|
|
|
5
5
|
accessToken?: string;
|
|
6
6
|
clientId?: string;
|
|
7
7
|
deviceName?: string;
|
|
8
|
+
/** Directory for the audio file cache. When set, decoded audio is stored so
|
|
9
|
+
* subsequent plays of the same track skip the CDN download. */
|
|
10
|
+
cacheDir?: string;
|
|
11
|
+
/** Maximum size of the audio cache in megabytes. Only meaningful when
|
|
12
|
+
* `cacheDir` is set. Omit for no limit. */
|
|
13
|
+
cacheSizeLimitMb?: number;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/** Options for streaming a track. */
|