@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.lock +3597 -0
- package/Cargo.toml +32 -0
- package/README.md +105 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +151 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +3 -0
- package/package.json +64 -0
- 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/scripts/postinstall.js +59 -0
- package/scripts/prebuild.js +23 -0
- package/scripts/prepare-prebuilds.js +44 -0
- package/scripts/test-connect.js +113 -0
- package/scripts/test-service.js +205 -0
- package/scripts/test.js +112 -0
- package/scripts/update-librespot.sh +13 -0
- package/src/index.ts +232 -0
- package/src/lib.rs +2305 -0
- package/src/types.ts +129 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
const DIST_ENTRY = path.join(ROOT, 'dist', 'index.js');
|
|
8
|
+
|
|
9
|
+
function loadLibrary() {
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
12
|
+
return require(DIST_ENTRY);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error(
|
|
15
|
+
'Kan dist/index.js niet laden. Draai eerst `npm run build:debug` of `npm run build` zodat het native addon beschikbaar is.',
|
|
16
|
+
);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { startConnectDevice, loginWithAccessToken, setLogLevel } = loadLibrary();
|
|
22
|
+
|
|
23
|
+
function readCredentials(filePath) {
|
|
24
|
+
const resolved = path.resolve(filePath);
|
|
25
|
+
try {
|
|
26
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(`Kon credentials niet lezen van ${resolved}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function resolveCredentials(deviceName) {
|
|
33
|
+
if (process.env.SPOTIFY_CREDENTIALS_JSON) {
|
|
34
|
+
return process.env.SPOTIFY_CREDENTIALS_JSON;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.env.SPOTIFY_CREDENTIALS_PATH) {
|
|
38
|
+
return readCredentials(process.env.SPOTIFY_CREDENTIALS_PATH);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.SPOTIFY_ACCESS_TOKEN) {
|
|
42
|
+
console.info('Maak credentials aan via SPOTIFY_ACCESS_TOKEN...');
|
|
43
|
+
const res = await loginWithAccessToken(process.env.SPOTIFY_ACCESS_TOKEN, deviceName);
|
|
44
|
+
console.info(`Gebruiker: ${res.username}`);
|
|
45
|
+
return res.credentialsJson;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Geef credentials door via SPOTIFY_CREDENTIALS_JSON, SPOTIFY_CREDENTIALS_PATH of SPOTIFY_ACCESS_TOKEN.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const deviceName = process.env.SPOTIFY_DEVICE_NAME || 'node-librespot-connect';
|
|
55
|
+
const deviceId = process.env.SPOTIFY_DEVICE_ID || 'node-librespot-connect-id';
|
|
56
|
+
const durationMs =
|
|
57
|
+
Number(process.env.SPOTIFY_CONNECT_DURATION_MS || process.env.SPOTIFY_TEST_DURATION_MS) ||
|
|
58
|
+
30000;
|
|
59
|
+
|
|
60
|
+
setLogLevel(process.env.SPOTIFY_LOG_LEVEL || 'info');
|
|
61
|
+
|
|
62
|
+
const credentialsJson = await resolveCredentials(deviceName);
|
|
63
|
+
console.info(`Start Spotify Connect host voor "${deviceName}" (${deviceId})`);
|
|
64
|
+
|
|
65
|
+
let chunkCount = 0;
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
let events = 0;
|
|
68
|
+
|
|
69
|
+
const handle = await startConnectDevice(
|
|
70
|
+
credentialsJson,
|
|
71
|
+
deviceName,
|
|
72
|
+
deviceId,
|
|
73
|
+
(chunk) => {
|
|
74
|
+
chunkCount += 1;
|
|
75
|
+
bytes += chunk.length;
|
|
76
|
+
},
|
|
77
|
+
(event) => {
|
|
78
|
+
events += 1;
|
|
79
|
+
if (event.type === 'error') {
|
|
80
|
+
console.error('Event (error):', event);
|
|
81
|
+
} else if (event.type !== 'metric' && event.type !== 'health') {
|
|
82
|
+
console.info('Event:', event);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
(log) => {
|
|
86
|
+
if (log.level === 'error' || log.level === 'warn') {
|
|
87
|
+
console.info('Log:', log);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const stop = () => {
|
|
93
|
+
console.info(
|
|
94
|
+
`Stop Connect host na ~${durationMs}ms; ${chunkCount} chunks ontvangen (~${(bytes / 1024).toFixed(1)} KiB), ${events} events.`,
|
|
95
|
+
);
|
|
96
|
+
handle.stop();
|
|
97
|
+
handle.shutdown?.();
|
|
98
|
+
handle.close?.();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const timeout = setTimeout(stop, durationMs);
|
|
102
|
+
|
|
103
|
+
process.on('SIGINT', () => {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
stop();
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error('Connect test mislukt:', err);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
const DIST_ENTRY = path.join(ROOT, 'dist', 'index.js');
|
|
8
|
+
|
|
9
|
+
function loadLibrary() {
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
12
|
+
return require(DIST_ENTRY);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error(
|
|
15
|
+
'Kan dist/index.js niet laden. Draai eerst `npm run build:debug` of `npm run build` zodat het native addon beschikbaar is.',
|
|
16
|
+
);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { startConnectDevice, createSession, loginWithAccessToken, setLogLevel } = loadLibrary();
|
|
22
|
+
|
|
23
|
+
function readCredentials(filePath) {
|
|
24
|
+
const resolved = path.resolve(filePath);
|
|
25
|
+
try {
|
|
26
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(`Kon credentials niet lezen van ${resolved}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function resolveCredentials(deviceName) {
|
|
33
|
+
if (process.env.SPOTIFY_CREDENTIALS_JSON) {
|
|
34
|
+
return process.env.SPOTIFY_CREDENTIALS_JSON;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.env.SPOTIFY_CREDENTIALS_PATH) {
|
|
38
|
+
return readCredentials(process.env.SPOTIFY_CREDENTIALS_PATH);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.SPOTIFY_ACCESS_TOKEN) {
|
|
42
|
+
console.info('Maak credentials aan via SPOTIFY_ACCESS_TOKEN...');
|
|
43
|
+
const res = await loginWithAccessToken(process.env.SPOTIFY_ACCESS_TOKEN, deviceName);
|
|
44
|
+
console.info(`Gebruiker: ${res.username}`);
|
|
45
|
+
return res.credentialsJson;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Geef credentials door via SPOTIFY_CREDENTIALS_JSON, SPOTIFY_CREDENTIALS_PATH of SPOTIFY_ACCESS_TOKEN.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scheduleRestart(attempt, fn) {
|
|
54
|
+
const restartBackoffMs = [2000, 4000, 8000];
|
|
55
|
+
const delay = restartBackoffMs[Math.min(attempt, restartBackoffMs.length - 1)];
|
|
56
|
+
console.info(`Herstart Connect host over ${delay}ms (poging ${attempt + 1})`);
|
|
57
|
+
setTimeout(fn, delay);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runConnectHost(credentialsJson, deviceName, deviceId, durationMs) {
|
|
61
|
+
let handle = null;
|
|
62
|
+
let chunkCount = 0;
|
|
63
|
+
let bytes = 0;
|
|
64
|
+
let events = 0;
|
|
65
|
+
let restartAttempt = 0;
|
|
66
|
+
let stopping = false;
|
|
67
|
+
|
|
68
|
+
const start = async () => {
|
|
69
|
+
console.info(`Start Spotify Connect host voor "${deviceName}" (${deviceId})`);
|
|
70
|
+
handle = await startConnectDevice(
|
|
71
|
+
credentialsJson,
|
|
72
|
+
deviceName,
|
|
73
|
+
deviceId,
|
|
74
|
+
(chunk) => {
|
|
75
|
+
chunkCount += 1;
|
|
76
|
+
bytes += chunk.length;
|
|
77
|
+
},
|
|
78
|
+
(event) => {
|
|
79
|
+
events += 1;
|
|
80
|
+
if (event.type === 'error') {
|
|
81
|
+
console.error('Event (error):', event);
|
|
82
|
+
if (!stopping) {
|
|
83
|
+
handle.stop();
|
|
84
|
+
handle = null;
|
|
85
|
+
scheduleRestart(restartAttempt, start);
|
|
86
|
+
restartAttempt += 1;
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.type !== 'metric' && event.type !== 'health') {
|
|
91
|
+
console.info('Event:', event);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
(log) => {
|
|
95
|
+
if (log.level === 'error' || log.level === 'warn') {
|
|
96
|
+
console.info('Log:', log);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
restartAttempt = 0;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await start();
|
|
104
|
+
|
|
105
|
+
const stop = async (reason) => {
|
|
106
|
+
if (stopping) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
stopping = true;
|
|
110
|
+
if (handle) {
|
|
111
|
+
try {
|
|
112
|
+
handle.stop();
|
|
113
|
+
handle.shutdown?.();
|
|
114
|
+
handle.close?.();
|
|
115
|
+
} catch {
|
|
116
|
+
/* ignore */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.info(
|
|
120
|
+
`${reason}; ${chunkCount} chunks ontvangen (~${(bytes / 1024).toFixed(
|
|
121
|
+
1,
|
|
122
|
+
)} KiB), ${events} events.`,
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const timeout = setTimeout(() => stop(`Stop na ${durationMs}ms`), durationMs);
|
|
127
|
+
|
|
128
|
+
process.on('SIGINT', async () => {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
await stop('Stop door SIGINT');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runDirectStream(credentialsJson, deviceName, trackUri, durationMs) {
|
|
136
|
+
const bitrate = Number(process.env.SPOTIFY_BITRATE) || 160;
|
|
137
|
+
console.info(`Maak directe stream voor ${trackUri} op ${bitrate} kbps`);
|
|
138
|
+
const session = await createSession({ credentialsJson, deviceName });
|
|
139
|
+
let chunkCount = 0;
|
|
140
|
+
let bytes = 0;
|
|
141
|
+
|
|
142
|
+
const handle = session.streamTrack(
|
|
143
|
+
{ uri: trackUri, bitrate, emitEvents: true },
|
|
144
|
+
(chunk) => {
|
|
145
|
+
chunkCount += 1;
|
|
146
|
+
bytes += chunk.length;
|
|
147
|
+
},
|
|
148
|
+
(event) => {
|
|
149
|
+
if (event.type === 'error') {
|
|
150
|
+
console.error('Event (error):', event);
|
|
151
|
+
} else if (event.type === 'metric' || event.type === 'health') {
|
|
152
|
+
console.info('Event:', event);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
(log) => {
|
|
156
|
+
if (log.level === 'error' || log.level === 'warn') {
|
|
157
|
+
console.info('Log:', log);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const stop = async (reason) => {
|
|
163
|
+
console.info(
|
|
164
|
+
`${reason}; ${chunkCount} chunks ontvangen (~${(bytes / 1024).toFixed(1)} KiB).`,
|
|
165
|
+
);
|
|
166
|
+
handle.stop();
|
|
167
|
+
await session.close();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const timeout = setTimeout(() => stop(`Stop na ${durationMs}ms`), durationMs);
|
|
171
|
+
process.on('SIGINT', async () => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
await stop('Stop door SIGINT');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main() {
|
|
179
|
+
const deviceName = process.env.SPOTIFY_DEVICE_NAME || 'node-librespot-service';
|
|
180
|
+
const deviceId = process.env.SPOTIFY_DEVICE_ID || 'node-librespot-service-id';
|
|
181
|
+
const durationMs =
|
|
182
|
+
Number(process.env.SPOTIFY_SERVICE_DURATION_MS || process.env.SPOTIFY_TEST_DURATION_MS) ||
|
|
183
|
+
30000;
|
|
184
|
+
const trackUri = process.env.SPOTIFY_TRACK_URI;
|
|
185
|
+
const mode = process.env.SPOTIFY_SERVICE_MODE || (trackUri ? 'stream' : 'connect');
|
|
186
|
+
|
|
187
|
+
setLogLevel(process.env.SPOTIFY_LOG_LEVEL || 'info');
|
|
188
|
+
|
|
189
|
+
const credentialsJson = await resolveCredentials(deviceName);
|
|
190
|
+
|
|
191
|
+
if (mode === 'stream') {
|
|
192
|
+
if (!trackUri) {
|
|
193
|
+
throw new Error('Zet SPOTIFY_TRACK_URI voor stream mode.');
|
|
194
|
+
}
|
|
195
|
+
await runDirectStream(credentialsJson, deviceName, trackUri, durationMs);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await runConnectHost(credentialsJson, deviceName, deviceId, durationMs);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main().catch((err) => {
|
|
203
|
+
console.error('Service test mislukt:', err);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
package/scripts/test.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
const DIST_ENTRY = path.join(ROOT, 'dist', 'index.js');
|
|
8
|
+
|
|
9
|
+
function loadLibrary() {
|
|
10
|
+
try {
|
|
11
|
+
return require(DIST_ENTRY);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(
|
|
14
|
+
'Kan dist/index.js niet laden. Draai eerst `npm run build:debug` of `npm run build` zodat het native addon beschikbaar is.',
|
|
15
|
+
);
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { createSession, loginWithAccessToken, setLogLevel } = loadLibrary();
|
|
21
|
+
|
|
22
|
+
function readCredentials(filePath) {
|
|
23
|
+
const resolved = path.resolve(filePath);
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`Kon credentials niet lezen van ${resolved}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function resolveCredentials(deviceName) {
|
|
32
|
+
if (process.env.SPOTIFY_CREDENTIALS_JSON) {
|
|
33
|
+
return process.env.SPOTIFY_CREDENTIALS_JSON;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.SPOTIFY_CREDENTIALS_PATH) {
|
|
37
|
+
return readCredentials(process.env.SPOTIFY_CREDENTIALS_PATH);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (process.env.SPOTIFY_ACCESS_TOKEN) {
|
|
41
|
+
console.info('Maak credentials aan via SPOTIFY_ACCESS_TOKEN...');
|
|
42
|
+
const res = await loginWithAccessToken(process.env.SPOTIFY_ACCESS_TOKEN, deviceName);
|
|
43
|
+
console.info(`Gebruiker: ${res.username}`);
|
|
44
|
+
return res.credentialsJson;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Geef credentials door via SPOTIFY_CREDENTIALS_JSON, SPOTIFY_CREDENTIALS_PATH of SPOTIFY_ACCESS_TOKEN.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const trackUri = process.env.SPOTIFY_TRACK_URI;
|
|
54
|
+
if (!trackUri) {
|
|
55
|
+
throw new Error('Zet SPOTIFY_TRACK_URI op een Spotify track/episode URI om te testen.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const deviceName = process.env.SPOTIFY_DEVICE_NAME || 'node-librespot-test';
|
|
59
|
+
const bitrate = Number(process.env.SPOTIFY_BITRATE) || 160;
|
|
60
|
+
const durationMs = Number(process.env.SPOTIFY_TEST_DURATION_MS) || 5000;
|
|
61
|
+
|
|
62
|
+
setLogLevel(process.env.SPOTIFY_LOG_LEVEL || 'info');
|
|
63
|
+
|
|
64
|
+
const credentialsJson = await resolveCredentials(deviceName);
|
|
65
|
+
console.info('Sessies opzetten...');
|
|
66
|
+
const session = await createSession({ credentialsJson, deviceName });
|
|
67
|
+
console.info(`Stream ${trackUri} op ${bitrate} kbps voor ~${durationMs}ms`);
|
|
68
|
+
|
|
69
|
+
let chunkCount = 0;
|
|
70
|
+
let bytes = 0;
|
|
71
|
+
const streamHandle = session.streamTrack(
|
|
72
|
+
{ uri: trackUri, bitrate, emitEvents: true },
|
|
73
|
+
(chunk) => {
|
|
74
|
+
chunkCount += 1;
|
|
75
|
+
bytes += chunk.length;
|
|
76
|
+
},
|
|
77
|
+
(event) => {
|
|
78
|
+
if (event.type === 'error') {
|
|
79
|
+
console.error('Event (error):', event);
|
|
80
|
+
} else if (event.type === 'metric' || event.type === 'health') {
|
|
81
|
+
console.info('Event:', event);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
(log) => {
|
|
85
|
+
if (log.level === 'error' || log.level === 'warn') {
|
|
86
|
+
console.info('Log:', log);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const stop = async () => {
|
|
92
|
+
console.info(
|
|
93
|
+
`Stop na ${durationMs}ms; ${chunkCount} chunks ontvangen (~${(bytes / 1024).toFixed(1)} KiB).`,
|
|
94
|
+
);
|
|
95
|
+
streamHandle.stop();
|
|
96
|
+
await session.close();
|
|
97
|
+
console.info('Klaar.');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const timeout = setTimeout(stop, durationMs);
|
|
101
|
+
|
|
102
|
+
process.on('SIGINT', async () => {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
await stop();
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main().catch((err) => {
|
|
110
|
+
console.error('Test mislukt:', err);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
repo_dir="${script_dir}/../librespot-dev"
|
|
6
|
+
repo_url="https://github.com/librespot-org/librespot.git"
|
|
7
|
+
|
|
8
|
+
if [[ ! -d "${repo_dir}/.git" ]]; then
|
|
9
|
+
mkdir -p "${repo_dir}"
|
|
10
|
+
git clone "${repo_url}" "${repo_dir}"
|
|
11
|
+
else
|
|
12
|
+
git -C "${repo_dir}" pull --ff-only
|
|
13
|
+
fi
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ConnectHandle,
|
|
6
|
+
ConnectEvent,
|
|
7
|
+
CreateSessionOpts,
|
|
8
|
+
CredentialsResult,
|
|
9
|
+
LibrespotSession,
|
|
10
|
+
LogEvent,
|
|
11
|
+
StreamHandle,
|
|
12
|
+
StreamTrackOpts,
|
|
13
|
+
DownloadTrackOpts,
|
|
14
|
+
DownloadHandle,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
function detectLibc(): 'gnu' | 'musl' {
|
|
18
|
+
const glibcVersionRuntime =
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
process.report?.getReport?.()?.header?.glibcVersionRuntime;
|
|
21
|
+
return glibcVersionRuntime ? 'gnu' : 'musl';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function platformArchABI(): string {
|
|
25
|
+
const { platform, arch } = process;
|
|
26
|
+
if (platform === 'linux') {
|
|
27
|
+
return `linux-${arch}-${detectLibc()}`;
|
|
28
|
+
}
|
|
29
|
+
if (platform === 'darwin') {
|
|
30
|
+
return `darwin-${arch}`;
|
|
31
|
+
}
|
|
32
|
+
if (platform === 'win32') {
|
|
33
|
+
return `win32-${arch}-msvc`;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Unsupported platform ${platform}-${arch}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveNativeBinding() {
|
|
39
|
+
const override = process.env.LOX_LIBRESPOT_ADDON_PATH;
|
|
40
|
+
if (override && fs.existsSync(override)) {
|
|
41
|
+
return override;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prebuiltPath = path.join(
|
|
45
|
+
__dirname,
|
|
46
|
+
'..',
|
|
47
|
+
'prebuilds',
|
|
48
|
+
platformArchABI(),
|
|
49
|
+
'librespot_addon.node',
|
|
50
|
+
);
|
|
51
|
+
if (fs.existsSync(prebuiltPath)) {
|
|
52
|
+
return prebuiltPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const localBuildPath = path.join(__dirname, 'librespot_addon.node');
|
|
56
|
+
if (fs.existsSync(localBuildPath)) {
|
|
57
|
+
return localBuildPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(
|
|
61
|
+
`librespot_addon.node not found for ${platformArchABI()}. ` +
|
|
62
|
+
'Install a prebuilt binary, build locally with "npm run build", ' +
|
|
63
|
+
'or point LOX_LIBRESPOT_ADDON_PATH to the compiled addon.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
68
|
+
const native = require(resolveNativeBinding()) as {
|
|
69
|
+
createSession(opts: CreateSessionOpts): Promise<LibrespotSession>;
|
|
70
|
+
setLogLevel(level: string): void;
|
|
71
|
+
loginWithAccessToken(
|
|
72
|
+
accessToken: string,
|
|
73
|
+
deviceName?: string,
|
|
74
|
+
): Promise<CredentialsResult>;
|
|
75
|
+
downloadTrack(
|
|
76
|
+
opts: DownloadTrackOpts,
|
|
77
|
+
onChunk: (chunk: Buffer) => void,
|
|
78
|
+
onLog?: (event: LogEvent) => void,
|
|
79
|
+
): Promise<DownloadHandle>;
|
|
80
|
+
startZeroconfLogin(
|
|
81
|
+
deviceId: string,
|
|
82
|
+
name?: string | null,
|
|
83
|
+
timeoutMs?: number | null,
|
|
84
|
+
): Promise<CredentialsResult>;
|
|
85
|
+
startConnectDevice(
|
|
86
|
+
credentialsPath: string,
|
|
87
|
+
name: string,
|
|
88
|
+
deviceId: string,
|
|
89
|
+
onChunk: (chunk: Buffer) => void,
|
|
90
|
+
onEvent?: (event: ConnectEvent) => void,
|
|
91
|
+
onLog?: (event: LogEvent) => void,
|
|
92
|
+
): Promise<ConnectHandle>;
|
|
93
|
+
startConnectDeviceWithToken(
|
|
94
|
+
accessToken: string,
|
|
95
|
+
clientId: string | undefined,
|
|
96
|
+
name: string,
|
|
97
|
+
deviceId: string,
|
|
98
|
+
onChunk: (chunk: Buffer) => void,
|
|
99
|
+
onEvent?: (event: ConnectEvent) => void,
|
|
100
|
+
onLog?: (event: LogEvent) => void,
|
|
101
|
+
): Promise<ConnectHandle>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function wrapStreamHandle(handle: StreamHandle) {
|
|
105
|
+
return {
|
|
106
|
+
stop: () => handle.stop(),
|
|
107
|
+
get sampleRate() {
|
|
108
|
+
return (handle as any).sampleRate ?? (handle as any).sample_rate ?? handle.sampleRate;
|
|
109
|
+
},
|
|
110
|
+
get channels() {
|
|
111
|
+
return (handle as any).channels ?? handle.channels;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function wrapSession(session: LibrespotSession) {
|
|
117
|
+
return {
|
|
118
|
+
streamTrack: (
|
|
119
|
+
opts: StreamTrackOpts,
|
|
120
|
+
onChunk: (chunk: Buffer) => void,
|
|
121
|
+
onEvent?: (event: ConnectEvent) => void,
|
|
122
|
+
onLog?: (event: LogEvent) => void,
|
|
123
|
+
) => {
|
|
124
|
+
const nativeOpts = {
|
|
125
|
+
uri: opts.uri,
|
|
126
|
+
startPositionMs: (opts as any).startPositionMs ?? (opts as any).start_position_ms,
|
|
127
|
+
bitrate: opts.bitrate,
|
|
128
|
+
output: (opts as any).output,
|
|
129
|
+
emitEvents: (opts as any).emitEvents ?? (opts as any).emit_events,
|
|
130
|
+
};
|
|
131
|
+
const handle = (session as any).streamTrack(nativeOpts, onChunk, onEvent, onLog);
|
|
132
|
+
return wrapStreamHandle(handle);
|
|
133
|
+
},
|
|
134
|
+
close: () => session.close(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createSession(opts: CreateSessionOpts): Promise<LibrespotSession> {
|
|
139
|
+
const nativeOpts = {
|
|
140
|
+
accessToken: (opts as any).accessToken ?? (opts as any).access_token,
|
|
141
|
+
clientId: (opts as any).clientId ?? (opts as any).client_id,
|
|
142
|
+
deviceName: (opts as any).deviceName ?? (opts as any).device_name,
|
|
143
|
+
};
|
|
144
|
+
return native.createSession(nativeOpts as CreateSessionOpts).then((sess) => wrapSession(sess) as any);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function loginWithAccessToken(
|
|
148
|
+
accessToken: string,
|
|
149
|
+
deviceName?: string,
|
|
150
|
+
): Promise<CredentialsResult> {
|
|
151
|
+
return native.loginWithAccessToken(accessToken, deviceName).then((res: any) => {
|
|
152
|
+
const credentialsJson = res.credentialsJson ?? res.credentials_json;
|
|
153
|
+
return {
|
|
154
|
+
...res,
|
|
155
|
+
credentialsJson,
|
|
156
|
+
credentials_json: credentialsJson ?? res.credentials_json,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function startZeroconfLogin(
|
|
162
|
+
deviceId: string,
|
|
163
|
+
name?: string,
|
|
164
|
+
timeoutMs?: number,
|
|
165
|
+
): Promise<CredentialsResult> {
|
|
166
|
+
return native.startZeroconfLogin(deviceId, name, timeoutMs).then((res: any) => {
|
|
167
|
+
const credentialsJson = res.credentialsJson ?? res.credentials_json;
|
|
168
|
+
return {
|
|
169
|
+
...res,
|
|
170
|
+
credentialsJson,
|
|
171
|
+
credentials_json: credentialsJson ?? res.credentials_json,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function startConnectDevice(
|
|
177
|
+
credentialsPath: string,
|
|
178
|
+
name: string,
|
|
179
|
+
deviceId: string,
|
|
180
|
+
onChunk: (chunk: Buffer) => void,
|
|
181
|
+
onEvent?: (event: ConnectEvent) => void,
|
|
182
|
+
onLog?: (event: LogEvent) => void,
|
|
183
|
+
): Promise<ConnectHandle> {
|
|
184
|
+
// Legacy entrypoint kept for API compatibility; immediately fails.
|
|
185
|
+
return Promise.reject(
|
|
186
|
+
new Error('startConnectDevice is deprecated; use startConnectDeviceWithToken(accessToken, clientId, ...)'),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function startConnectDeviceWithToken(
|
|
191
|
+
accessToken: string,
|
|
192
|
+
clientId: string | undefined,
|
|
193
|
+
name: string,
|
|
194
|
+
deviceId: string,
|
|
195
|
+
onChunk: (chunk: Buffer) => void,
|
|
196
|
+
onEvent?: (event: ConnectEvent) => void,
|
|
197
|
+
onLog?: (event: LogEvent) => void,
|
|
198
|
+
): Promise<ConnectHandle> {
|
|
199
|
+
return Promise.resolve(
|
|
200
|
+
native.startConnectDeviceWithToken(accessToken, clientId, name, deviceId, onChunk, onEvent, onLog),
|
|
201
|
+
).then((handle: ConnectHandle & { sample_rate?: number }) => ({
|
|
202
|
+
stop: () => handle.stop(),
|
|
203
|
+
shutdown: () => handle.shutdown(),
|
|
204
|
+
close: () => handle.close(),
|
|
205
|
+
play: () => handle.play(),
|
|
206
|
+
pause: () => handle.pause(),
|
|
207
|
+
next: () => handle.next(),
|
|
208
|
+
prev: () => handle.prev(),
|
|
209
|
+
sampleRate: (handle as any).sampleRate ?? (handle as any).sample_rate ?? (handle as any).sampleRate,
|
|
210
|
+
channels: (handle as any).channels,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function setLogLevel(level: string): void {
|
|
215
|
+
native.setLogLevel(level);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function downloadTrack(
|
|
219
|
+
opts: DownloadTrackOpts,
|
|
220
|
+
onChunk: (chunk: Buffer) => void,
|
|
221
|
+
onLog?: (event: LogEvent) => void,
|
|
222
|
+
): Promise<DownloadHandle> {
|
|
223
|
+
const nativeOpts = {
|
|
224
|
+
uri: opts.uri,
|
|
225
|
+
bitrate: opts.bitrate,
|
|
226
|
+
};
|
|
227
|
+
return native.downloadTrack(nativeOpts, onChunk, onLog);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Export raw native binding for advanced use/debugging.
|
|
231
|
+
export { native };
|
|
232
|
+
export * from './types';
|