bose-soundtouch 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Grant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # bose-soundtouch
2
+
3
+ A JavaScript/TypeScript library for controlling Bose SoundTouch speakers via the local REST API.
4
+
5
+ ## Background
6
+
7
+ On January 7, 2026, Bose announced that cloud support for SoundTouch products will end on May 6, 2026. After this date:
8
+
9
+ **What will continue to work:**
10
+
11
+ - Streaming via Bluetooth, AirPlay, Spotify Connect, and AUX
12
+ - Setting up and configuring your system
13
+ - Remote control features (play, pause, skip, volume)
14
+ - Grouping multiple speakers together
15
+
16
+ **What will stop working:**
17
+
18
+ - Presets (preset buttons and app presets)
19
+ - Browsing music services from the SoundTouch app
20
+
21
+ As part of this transition, Bose released their SoundTouch API Documentation to enable independent developers to create their own SoundTouch-compatible tools.
22
+
23
+ This library provides a clean, TypeScript-friendly interface to control SoundTouch speakers over your local network, ensuring your speakers remain fully functional even after cloud services end.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install bose-soundtouch
29
+ ```
30
+
31
+ or
32
+
33
+ ```bash
34
+ yarn add bose-soundtouch
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```typescript
40
+ import { SoundTouch } from "bose-soundtouch";
41
+
42
+ // Connect to a speaker by IP address
43
+ const speaker = new SoundTouch("192.168.1.100");
44
+
45
+ // Get device info
46
+ const info = await speaker.getInfo();
47
+ console.log(`Connected to: ${info.name} (${info.type})`);
48
+
49
+ // Control playback
50
+ await speaker.play();
51
+ await speaker.setVolume(30);
52
+
53
+ // Check what's playing
54
+ const nowPlaying = await speaker.getNowPlaying();
55
+ console.log(`Playing: ${nowPlaying.track} by ${nowPlaying.artist}`);
56
+ ```
57
+
58
+ ## Demo Script
59
+
60
+ A comprehensive demo script is included to test the library against a real device:
61
+
62
+ ```bash
63
+ npm run demo 192.168.1.100
64
+ ```
65
+
66
+ or
67
+
68
+ ```bash
69
+ ts-node demo.ts 192.168.1.100
70
+ ```
71
+
72
+ The demo walks through device info, capabilities, sources, presets, volume control, mute, and playback controls.
73
+
74
+ ## Features
75
+
76
+ - Control playback (play, pause, stop, next/previous track)
77
+ - Adjust volume and mute
78
+ - Select presets (1-6)
79
+ - Select sources (AUX, Bluetooth, etc.)
80
+ - Get now playing information
81
+ - Control bass and tone settings
82
+ - Full TypeScript support with type definitions
83
+ - Error handling with custom error classes
84
+
85
+ ## Usage Examples
86
+
87
+ ### Playback Control
88
+
89
+ ```typescript
90
+ import { SoundTouch } from "bose-soundtouch";
91
+
92
+ const speaker = new SoundTouch("192.168.1.100");
93
+
94
+ await speaker.play();
95
+ await speaker.pause();
96
+ await speaker.playPause(); // Toggle
97
+ await speaker.stop();
98
+ await speaker.nextTrack();
99
+ await speaker.previousTrack();
100
+ ```
101
+
102
+ ### Volume Control
103
+
104
+ ```typescript
105
+ import { SoundTouch } from "bose-soundtouch";
106
+
107
+ const speaker = new SoundTouch("192.168.1.100");
108
+
109
+ // Get current volume
110
+ const volume = await speaker.getVolume();
111
+ console.log(`Volume: ${volume.actualvolume}, Muted: ${volume.muteenabled}`);
112
+
113
+ // Set volume (0-100)
114
+ await speaker.setVolume(50);
115
+
116
+ // Mute/unmute
117
+ await speaker.mute();
118
+ await speaker.unmute();
119
+
120
+ // Volume up/down buttons
121
+ await speaker.volumeUp();
122
+ await speaker.volumeDown();
123
+ ```
124
+
125
+ ### Presets
126
+
127
+ ```typescript
128
+ import { SoundTouch } from "bose-soundtouch";
129
+
130
+ const speaker = new SoundTouch("192.168.1.100");
131
+
132
+ // Get all presets
133
+ const presets = await speaker.getPresets();
134
+ for (const preset of presets.preset) {
135
+ if (preset.contentItem) {
136
+ console.log(`Preset ${preset.id}: ${preset.contentItem.itemName}`);
137
+ }
138
+ }
139
+
140
+ // Select a preset
141
+ await speaker.selectPreset(1);
142
+ ```
143
+
144
+ ### Now Playing
145
+
146
+ ```typescript
147
+ import { SoundTouch, PlayStatus } from "bose-soundtouch";
148
+
149
+ const speaker = new SoundTouch("192.168.1.100");
150
+
151
+ const now = await speaker.getNowPlaying();
152
+
153
+ console.log(`Source: ${now.source}`);
154
+ console.log(`Track: ${now.track}`);
155
+ console.log(`Artist: ${now.artist}`);
156
+ console.log(`Album: ${now.album}`);
157
+
158
+ if (now.playStatus === PlayStatus.PLAY_STATE) {
159
+ console.log("Currently playing");
160
+ } else if (now.playStatus === PlayStatus.PAUSE_STATE) {
161
+ console.log("Paused");
162
+ }
163
+ ```
164
+
165
+ ### Source Selection
166
+
167
+ ```typescript
168
+ import { SoundTouch } from "bose-soundtouch";
169
+
170
+ const speaker = new SoundTouch("192.168.1.100");
171
+
172
+ // List available sources
173
+ const sources = await speaker.getSources();
174
+ for (const source of sources.sourceItem) {
175
+ console.log(`${source.source}: ${source.status}`);
176
+ }
177
+
178
+ // Select a source
179
+ await speaker.selectSource("AUX", "AUX");
180
+ await speaker.selectSource("BLUETOOTH");
181
+ ```
182
+
183
+ ### Device Information
184
+
185
+ ```typescript
186
+ import { SoundTouch } from "bose-soundtouch";
187
+
188
+ const speaker = new SoundTouch("192.168.1.100");
189
+
190
+ const info = await speaker.getInfo();
191
+
192
+ console.log(`Name: ${info.name}`);
193
+ console.log(`Type: ${info.type}`);
194
+ console.log(`Device ID: ${info.deviceID}`);
195
+
196
+ for (const net of info.networkInfo) {
197
+ console.log(` ${net.type}: ${net.ipAddress}`);
198
+ }
199
+ ```
200
+
201
+ ### Raw Key Press
202
+
203
+ For advanced use cases, you can send raw key presses:
204
+
205
+ ```typescript
206
+ import { SoundTouch, KeyValue } from "bose-soundtouch";
207
+
208
+ const speaker = new SoundTouch("192.168.1.100");
209
+
210
+ // Using enum
211
+ await speaker.sendKey(KeyValue.THUMBS_UP);
212
+
213
+ // Using string
214
+ await speaker.sendKey("POWER");
215
+ ```
216
+
217
+ ### Error Handling
218
+
219
+ ```typescript
220
+ import {
221
+ SoundTouch,
222
+ ConnectionError,
223
+ TimeoutError,
224
+ ApiError,
225
+ } from "bose-soundtouch";
226
+
227
+ try {
228
+ const speaker = new SoundTouch("192.168.1.100", 8090, 5.0);
229
+ await speaker.setVolume(50);
230
+ } catch (error) {
231
+ if (error instanceof ConnectionError) {
232
+ console.error("Could not connect to speaker");
233
+ } else if (error instanceof TimeoutError) {
234
+ console.error("Request timed out");
235
+ } else if (error instanceof ApiError) {
236
+ console.error(`API error: ${error.errorName} (code ${error.errorCode})`);
237
+ } else {
238
+ console.error("Unknown error:", error);
239
+ }
240
+ }
241
+ ```
242
+
243
+ ## API Reference
244
+
245
+ ### SoundTouch Class
246
+
247
+ ```typescript
248
+ new SoundTouch(
249
+ host: string, // IP address or hostname
250
+ port?: number, // HTTP port (default: 8090)
251
+ timeout?: number // Request timeout in seconds (default: 10.0)
252
+ )
253
+ ```
254
+
255
+ ### Methods
256
+
257
+ | Method | Description |
258
+ | ---------------------------------- | -------------------------- |
259
+ | `getInfo()` | Get device information |
260
+ | `getCapabilities()` | Get device capabilities |
261
+ | `setName(name: string)` | Set device name |
262
+ | `getNowPlaying()` | Get current playback state |
263
+ | `getSources()` | Get available sources |
264
+ | `selectSource(source, account?)` | Select a source |
265
+ | `getVolume()` | Get volume state |
266
+ | `setVolume(level: number)` | Set volume (0-100) |
267
+ | `mute()` / `unmute()` | Mute/unmute |
268
+ | `volumeUp()` / `volumeDown()` | Adjust volume |
269
+ | `getPresets()` | Get preset slots |
270
+ | `selectPreset(presetId: number)` | Select preset (1-6) |
271
+ | `play()` / `pause()` / `stop()` | Playback control |
272
+ | `playPause()` | Toggle play/pause |
273
+ | `nextTrack()` / `previousTrack()` | Track navigation |
274
+ | `getBass()` / `setBass(level)` | Bass control |
275
+ | `getTone()` | Get tone settings |
276
+ | `sendKey(key: KeyValue \| string)` | Send raw key press |
277
+
278
+ ### Types
279
+
280
+ The library exports TypeScript types and enums:
281
+
282
+ - `DeviceInfo` - Device information structure
283
+ - `Capabilities` - Device capabilities
284
+ - `Presets` - Preset slots
285
+ - `Sources` - Available sources
286
+ - `Volume` - Volume state
287
+ - `NowPlaying` - Current playback information
288
+ - `Bass` - Bass settings
289
+ - `Tone` - Tone settings (bass and treble)
290
+ - `PlayStatus` - Playback status enum
291
+ - `KeyValue` - Key press enum
292
+
293
+ ### Error Classes
294
+
295
+ - `ConnectionError` - Network connection errors
296
+ - `TimeoutError` - Request timeout errors
297
+ - `ApiError` - API error responses
298
+
299
+ ## Requirements
300
+
301
+ - Node.js 18.0.0 or higher
302
+ - TypeScript 5.0+ (for TypeScript projects)
303
+
304
+ ## Development
305
+
306
+ ```bash
307
+ # Install dependencies
308
+ npm install
309
+
310
+ # Build the project
311
+ npm run build
312
+
313
+ # Run in watch mode
314
+ npm run dev
315
+
316
+ # Run demo
317
+ npm run demo <ip-address>
318
+ ```
319
+
320
+ ## License
321
+
322
+ MIT License
323
+
324
+ ---
325
+
326
+ Not affiliated with, endorsed, sponsored, or approved by Bose. Bose and SoundTouch are trademarks of Bose Corporation.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Custom error classes for SoundTouch API
3
+ */
4
+ export declare class ConnectionError extends Error {
5
+ readonly cause?: Error | undefined;
6
+ constructor(message: string, cause?: Error | undefined);
7
+ }
8
+ export declare class TimeoutError extends Error {
9
+ constructor(message?: string);
10
+ }
11
+ export interface ApiErrorResponse {
12
+ error: {
13
+ name: string;
14
+ code: number;
15
+ message?: string;
16
+ };
17
+ }
18
+ export declare class ApiError extends Error {
19
+ readonly errorName: string;
20
+ readonly errorCode: number;
21
+ constructor(errorName: string, errorCode: number, message?: string);
22
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * Custom error classes for SoundTouch API
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ApiError = exports.TimeoutError = exports.ConnectionError = void 0;
7
+ class ConnectionError extends Error {
8
+ cause;
9
+ constructor(message, cause) {
10
+ super(message);
11
+ this.cause = cause;
12
+ this.name = "ConnectionError";
13
+ Object.setPrototypeOf(this, ConnectionError.prototype);
14
+ }
15
+ }
16
+ exports.ConnectionError = ConnectionError;
17
+ class TimeoutError extends Error {
18
+ constructor(message = "Request timed out") {
19
+ super(message);
20
+ this.name = "TimeoutError";
21
+ Object.setPrototypeOf(this, TimeoutError.prototype);
22
+ }
23
+ }
24
+ exports.TimeoutError = TimeoutError;
25
+ class ApiError extends Error {
26
+ errorName;
27
+ errorCode;
28
+ constructor(errorName, errorCode, message) {
29
+ super(message || `API error: ${errorName} (code ${errorCode})`);
30
+ this.errorName = errorName;
31
+ this.errorCode = errorCode;
32
+ this.name = "ApiError";
33
+ Object.setPrototypeOf(this, ApiError.prototype);
34
+ }
35
+ }
36
+ exports.ApiError = ApiError;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * bose-soundtouch
3
+ * A TypeScript library for controlling Bose SoundTouch speakers
4
+ */
5
+ export { SoundTouch } from "./soundtouch";
6
+ export * from "./types";
7
+ export * from "./errors";
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * bose-soundtouch
4
+ * A TypeScript library for controlling Bose SoundTouch speakers
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.SoundTouch = void 0;
22
+ var soundtouch_1 = require("./soundtouch");
23
+ Object.defineProperty(exports, "SoundTouch", { enumerable: true, get: function () { return soundtouch_1.SoundTouch; } });
24
+ __exportStar(require("./types"), exports);
25
+ __exportStar(require("./errors"), exports);
@@ -0,0 +1,134 @@
1
+ import { DeviceInfo, Capabilities, Presets, Sources, Volume, NowPlaying, Bass, Tone, KeyValue } from "./types";
2
+ /**
3
+ * Main class for controlling Bose SoundTouch speakers
4
+ */
5
+ export declare class SoundTouch {
6
+ private host;
7
+ private port;
8
+ private timeout;
9
+ private client;
10
+ private baseUrl;
11
+ /**
12
+ * Create a new SoundTouch instance
13
+ * @param host - IP address or hostname of the speaker
14
+ * @param port - HTTP port (default: 8090)
15
+ * @param timeout - Request timeout in seconds (default: 10.0)
16
+ */
17
+ constructor(host: string, port?: number, timeout?: number);
18
+ /**
19
+ * Handle errors and convert to appropriate error types
20
+ */
21
+ private handleError;
22
+ /**
23
+ * Parse XML string to JavaScript object
24
+ */
25
+ private parseXml;
26
+ /**
27
+ * Make a GET request and parse XML response
28
+ */
29
+ private get;
30
+ /**
31
+ * Make a POST request with XML body
32
+ */
33
+ private post;
34
+ /**
35
+ * Get device information
36
+ */
37
+ getInfo(): Promise<DeviceInfo>;
38
+ /**
39
+ * Get device capabilities
40
+ */
41
+ getCapabilities(): Promise<Capabilities>;
42
+ /**
43
+ * Set device name
44
+ */
45
+ setName(name: string): Promise<void>;
46
+ /**
47
+ * Get current playback state
48
+ */
49
+ getNowPlaying(): Promise<NowPlaying>;
50
+ /**
51
+ * Get available sources
52
+ */
53
+ getSources(): Promise<Sources>;
54
+ /**
55
+ * Select a source
56
+ */
57
+ selectSource(source: string, sourceAccount?: string): Promise<void>;
58
+ /**
59
+ * Get volume state
60
+ */
61
+ getVolume(): Promise<Volume>;
62
+ /**
63
+ * Set volume level (0-100)
64
+ */
65
+ setVolume(level: number): Promise<void>;
66
+ /**
67
+ * Mute the speaker
68
+ */
69
+ mute(): Promise<void>;
70
+ /**
71
+ * Unmute the speaker
72
+ */
73
+ unmute(): Promise<void>;
74
+ /**
75
+ * Increase volume
76
+ */
77
+ volumeUp(): Promise<void>;
78
+ /**
79
+ * Decrease volume
80
+ */
81
+ volumeDown(): Promise<void>;
82
+ /**
83
+ * Get presets
84
+ */
85
+ getPresets(): Promise<Presets>;
86
+ /**
87
+ * Select a preset (1-6)
88
+ */
89
+ selectPreset(presetId: number): Promise<void>;
90
+ /**
91
+ * Play
92
+ */
93
+ play(): Promise<void>;
94
+ /**
95
+ * Pause
96
+ */
97
+ pause(): Promise<void>;
98
+ /**
99
+ * Toggle play/pause
100
+ */
101
+ playPause(): Promise<void>;
102
+ /**
103
+ * Stop
104
+ */
105
+ stop(): Promise<void>;
106
+ /**
107
+ * Next track
108
+ */
109
+ nextTrack(): Promise<void>;
110
+ /**
111
+ * Previous track
112
+ */
113
+ previousTrack(): Promise<void>;
114
+ /**
115
+ * Get bass level
116
+ */
117
+ getBass(): Promise<Bass>;
118
+ /**
119
+ * Set bass level (-10 to 10)
120
+ */
121
+ setBass(level: number): Promise<void>;
122
+ /**
123
+ * Get tone settings (bass and treble)
124
+ */
125
+ getTone(): Promise<Tone>;
126
+ /**
127
+ * Send a raw key press
128
+ */
129
+ sendKey(key: KeyValue | string): Promise<void>;
130
+ /**
131
+ * Escape XML special characters
132
+ */
133
+ private escapeXml;
134
+ }
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SoundTouch = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const xml2js_1 = require("xml2js");
9
+ const types_1 = require("./types");
10
+ const errors_1 = require("./errors");
11
+ /**
12
+ * Main class for controlling Bose SoundTouch speakers
13
+ */
14
+ class SoundTouch {
15
+ host;
16
+ port;
17
+ timeout;
18
+ client;
19
+ baseUrl;
20
+ /**
21
+ * Create a new SoundTouch instance
22
+ * @param host - IP address or hostname of the speaker
23
+ * @param port - HTTP port (default: 8090)
24
+ * @param timeout - Request timeout in seconds (default: 10.0)
25
+ */
26
+ constructor(host, port = 8090, timeout = 10.0) {
27
+ this.host = host;
28
+ this.port = port;
29
+ this.timeout = timeout;
30
+ this.baseUrl = `http://${host}:${port}`;
31
+ this.client = axios_1.default.create({
32
+ baseURL: this.baseUrl,
33
+ timeout: timeout * 1000,
34
+ headers: {
35
+ "Content-Type": "application/xml",
36
+ },
37
+ });
38
+ }
39
+ /**
40
+ * Handle errors and convert to appropriate error types
41
+ */
42
+ handleError(error) {
43
+ if (axios_1.default.isAxiosError(error)) {
44
+ const axiosError = error;
45
+ if (axiosError.code === "ECONNREFUSED" || axiosError.code === "ENOTFOUND") {
46
+ throw new errors_1.ConnectionError(`Could not connect to speaker at ${this.baseUrl}`, axiosError);
47
+ }
48
+ if (axiosError.code === "ETIMEDOUT" || axiosError.code === "ECONNABORTED") {
49
+ throw new errors_1.TimeoutError(`Request to ${this.baseUrl} timed out`);
50
+ }
51
+ // Check for API error response
52
+ if (axiosError.response?.data) {
53
+ const data = axiosError.response.data;
54
+ if (typeof data === "object" && "error" in data) {
55
+ const errorData = data;
56
+ throw new errors_1.ApiError(errorData.error.name, errorData.error.code, errorData.error.message);
57
+ }
58
+ }
59
+ throw new errors_1.ConnectionError(`Request failed: ${axiosError.message}`, axiosError);
60
+ }
61
+ throw error;
62
+ }
63
+ /**
64
+ * Parse XML string to JavaScript object
65
+ */
66
+ async parseXml(xml) {
67
+ return new Promise((resolve, reject) => {
68
+ (0, xml2js_1.parseString)(xml, {
69
+ explicitArray: false,
70
+ mergeAttrs: true,
71
+ explicitCharkey: false,
72
+ trim: true,
73
+ normalize: true,
74
+ ignoreAttrs: false,
75
+ attrNameProcessors: [],
76
+ tagNameProcessors: [],
77
+ }, (err, result) => {
78
+ if (err) {
79
+ reject(new Error(`Failed to parse XML: ${err.message}`));
80
+ }
81
+ else {
82
+ resolve(result);
83
+ }
84
+ });
85
+ });
86
+ }
87
+ /**
88
+ * Make a GET request and parse XML response
89
+ */
90
+ async get(endpoint) {
91
+ try {
92
+ const response = await this.client.get(endpoint, {
93
+ responseType: "text",
94
+ });
95
+ const parsed = await this.parseXml(response.data);
96
+ return parsed;
97
+ }
98
+ catch (error) {
99
+ this.handleError(error);
100
+ }
101
+ }
102
+ /**
103
+ * Make a POST request with XML body
104
+ */
105
+ async post(endpoint, body) {
106
+ try {
107
+ await this.client.post(endpoint, body);
108
+ }
109
+ catch (error) {
110
+ this.handleError(error);
111
+ }
112
+ }
113
+ /**
114
+ * Get device information
115
+ */
116
+ async getInfo() {
117
+ const data = await this.get("/info");
118
+ // Normalize networkInfo to always be an array
119
+ if (data.info.networkInfo && !Array.isArray(data.info.networkInfo)) {
120
+ data.info.networkInfo = [data.info.networkInfo];
121
+ }
122
+ return data.info;
123
+ }
124
+ /**
125
+ * Get device capabilities
126
+ */
127
+ async getCapabilities() {
128
+ const data = await this.get("/capabilities");
129
+ // Normalize capabilities array
130
+ if (data.capabilities.capability && !Array.isArray(data.capabilities.capability)) {
131
+ data.capabilities.capability = [data.capabilities.capability];
132
+ }
133
+ console.log(data.capabilities.capability);
134
+ return data.capabilities;
135
+ }
136
+ /**
137
+ * Set device name
138
+ */
139
+ async setName(name) {
140
+ const xml = `<name>${this.escapeXml(name)}</name>`;
141
+ await this.post("/name", xml);
142
+ }
143
+ /**
144
+ * Get current playback state
145
+ */
146
+ async getNowPlaying() {
147
+ const data = await this.get("/now_playing");
148
+ return data.nowPlaying;
149
+ }
150
+ /**
151
+ * Get available sources
152
+ */
153
+ async getSources() {
154
+ const data = await this.get("/sources");
155
+ // Normalize sourceItem array
156
+ if (data.sources.sourceItem && !Array.isArray(data.sources.sourceItem)) {
157
+ data.sources.sourceItem = [data.sources.sourceItem];
158
+ }
159
+ return data.sources;
160
+ }
161
+ /**
162
+ * Select a source
163
+ */
164
+ async selectSource(source, sourceAccount) {
165
+ let xml = `<ContentItem source="${this.escapeXml(source)}"`;
166
+ if (sourceAccount) {
167
+ xml += ` sourceAccount="${this.escapeXml(sourceAccount)}"`;
168
+ }
169
+ xml += ` location=""></ContentItem>`;
170
+ await this.post("/select", xml);
171
+ }
172
+ /**
173
+ * Get volume state
174
+ */
175
+ async getVolume() {
176
+ const data = await this.get("/volume");
177
+ return data.volume;
178
+ }
179
+ /**
180
+ * Set volume level (0-100)
181
+ */
182
+ async setVolume(level) {
183
+ if (level < 0 || level > 100) {
184
+ throw new Error("Volume level must be between 0 and 100");
185
+ }
186
+ const xml = `<volume>${level}</volume>`;
187
+ await this.post("/volume", xml);
188
+ }
189
+ /**
190
+ * Mute the speaker
191
+ */
192
+ async mute() {
193
+ const xml = "<volume>mute</volume>";
194
+ await this.post("/volume", xml);
195
+ }
196
+ /**
197
+ * Unmute the speaker
198
+ */
199
+ async unmute() {
200
+ const xml = "<volume>unmute</volume>";
201
+ await this.post("/volume", xml);
202
+ }
203
+ /**
204
+ * Increase volume
205
+ */
206
+ async volumeUp() {
207
+ await this.post("/volume", "<volume>volumeUp</volume>");
208
+ }
209
+ /**
210
+ * Decrease volume
211
+ */
212
+ async volumeDown() {
213
+ await this.post("/volume", "<volume>volumeDown</volume>");
214
+ }
215
+ /**
216
+ * Get presets
217
+ */
218
+ async getPresets() {
219
+ const data = await this.get("/presets");
220
+ // Normalize preset array
221
+ if (data.presets.preset && !Array.isArray(data.presets.preset)) {
222
+ data.presets.preset = [data.presets.preset];
223
+ }
224
+ return data.presets;
225
+ }
226
+ /**
227
+ * Select a preset (1-6)
228
+ */
229
+ async selectPreset(presetId) {
230
+ if (presetId < 1 || presetId > 6) {
231
+ throw new Error("Preset ID must be between 1 and 6");
232
+ }
233
+ const key = `PRESET_${presetId}`;
234
+ await this.sendKey(key);
235
+ }
236
+ /**
237
+ * Play
238
+ */
239
+ async play() {
240
+ await this.sendKey(types_1.KeyValue.PLAY);
241
+ }
242
+ /**
243
+ * Pause
244
+ */
245
+ async pause() {
246
+ await this.sendKey(types_1.KeyValue.PAUSE);
247
+ }
248
+ /**
249
+ * Toggle play/pause
250
+ */
251
+ async playPause() {
252
+ const nowPlaying = await this.getNowPlaying();
253
+ if (nowPlaying.playStatus === types_1.PlayStatus.PLAY_STATE) {
254
+ await this.pause();
255
+ }
256
+ else {
257
+ await this.play();
258
+ }
259
+ }
260
+ /**
261
+ * Stop
262
+ */
263
+ async stop() {
264
+ await this.sendKey(types_1.KeyValue.PAUSE);
265
+ }
266
+ /**
267
+ * Next track
268
+ */
269
+ async nextTrack() {
270
+ await this.sendKey(types_1.KeyValue.NEXT_TRACK);
271
+ }
272
+ /**
273
+ * Previous track
274
+ */
275
+ async previousTrack() {
276
+ await this.sendKey(types_1.KeyValue.PREV_TRACK);
277
+ }
278
+ /**
279
+ * Get bass level
280
+ */
281
+ async getBass() {
282
+ const data = await this.get("/bass");
283
+ return data.bass;
284
+ }
285
+ /**
286
+ * Set bass level (-10 to 10)
287
+ */
288
+ async setBass(level) {
289
+ if (level < -10 || level > 10) {
290
+ throw new Error("Bass level must be between -10 and 10");
291
+ }
292
+ const xml = `<bass>${level}</bass>`;
293
+ await this.post("/bass", xml);
294
+ }
295
+ /**
296
+ * Get tone settings (bass and treble)
297
+ */
298
+ async getTone() {
299
+ const data = await this.get("/tone");
300
+ return data.tone;
301
+ }
302
+ /**
303
+ * Send a raw key press
304
+ */
305
+ async sendKey(key) {
306
+ const keyValue = typeof key === "string" ? key : key;
307
+ const xml = `<key state="press" sender="Gabbo">${keyValue}</key>`;
308
+ await this.post("/key", xml);
309
+ }
310
+ /**
311
+ * Escape XML special characters
312
+ */
313
+ escapeXml(unsafe) {
314
+ return unsafe
315
+ .replace(/&/g, "&amp;")
316
+ .replace(/</g, "&lt;")
317
+ .replace(/>/g, "&gt;")
318
+ .replace(/"/g, "&quot;")
319
+ .replace(/'/g, "&apos;");
320
+ }
321
+ }
322
+ exports.SoundTouch = SoundTouch;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Type definitions for Bose SoundTouch API responses
3
+ */
4
+ export declare enum PlayStatus {
5
+ PLAY_STATE = "PLAY_STATE",
6
+ PAUSE_STATE = "PAUSE_STATE",
7
+ STOP_STATE = "STOP_STATE",
8
+ BUFFERING_STATE = "BUFFERING_STATE"
9
+ }
10
+ export declare enum KeyValue {
11
+ POWER = "POWER",
12
+ PLAY = "PLAY",
13
+ PAUSE = "PAUSE",
14
+ PREV_TRACK = "PREV_TRACK",
15
+ NEXT_TRACK = "NEXT_TRACK",
16
+ THUMBS_UP = "THUMBS_UP",
17
+ THUMBS_DOWN = "THUMBS_DOWN",
18
+ BOOKMARK = "BOOKMARK",
19
+ PRESET_1 = "PRESET_1",
20
+ PRESET_2 = "PRESET_2",
21
+ PRESET_3 = "PRESET_3",
22
+ PRESET_4 = "PRESET_4",
23
+ PRESET_5 = "PRESET_5",
24
+ PRESET_6 = "PRESET_6",
25
+ AUX_INPUT = "AUX_INPUT",
26
+ SHUFFLE_OFF = "SHUFFLE_OFF",
27
+ SHUFFLE_ON = "SHUFFLE_ON",
28
+ REPEAT_OFF = "REPEAT_OFF",
29
+ REPEAT_ONE = "REPEAT_ONE",
30
+ REPEAT_ALL = "REPEAT_ALL",
31
+ ADD_FAVORITE = "ADD_FAVORITE",
32
+ REMOVE_FAVORITE = "REMOVE_FAVORITE",
33
+ INVALID_KEY = "INVALID_KEY"
34
+ }
35
+ export interface NetworkInfo {
36
+ type: string;
37
+ macAddress: string;
38
+ ipAddress: string;
39
+ }
40
+ export interface DeviceInfo {
41
+ deviceID: string;
42
+ name: string;
43
+ type: string;
44
+ networkInfo: NetworkInfo[];
45
+ }
46
+ export interface Capability {
47
+ name: string;
48
+ value: string;
49
+ }
50
+ export interface Capabilities {
51
+ capability: Capability[];
52
+ }
53
+ export interface ContentItem {
54
+ source: string;
55
+ sourceAccount?: string;
56
+ location?: string;
57
+ isPresetable?: boolean;
58
+ itemName?: string;
59
+ containerArt?: string;
60
+ type?: string;
61
+ }
62
+ export interface Preset {
63
+ id: string;
64
+ contentItem?: ContentItem;
65
+ }
66
+ export interface Presets {
67
+ preset: Preset[];
68
+ }
69
+ export interface Source {
70
+ source: string;
71
+ sourceAccount?: string;
72
+ status: string;
73
+ isLocal?: boolean;
74
+ multiroomallowed?: boolean;
75
+ }
76
+ export interface Sources {
77
+ sourceItem: Source[];
78
+ }
79
+ export interface Volume {
80
+ targetvolume: number;
81
+ actualvolume: number;
82
+ muteenabled: boolean;
83
+ }
84
+ export interface NowPlaying {
85
+ source: string;
86
+ ContentItem?: ContentItem;
87
+ track?: string;
88
+ artist?: string;
89
+ album?: string;
90
+ stationName?: string;
91
+ art?: string;
92
+ artImageStatus?: string;
93
+ playStatus?: PlayStatus;
94
+ skipEnabled?: boolean;
95
+ skipPreviousEnabled?: boolean;
96
+ favoriteEnabled?: boolean;
97
+ isFavorite?: boolean;
98
+ stationLocation?: string;
99
+ time?: number;
100
+ totalTime?: number;
101
+ }
102
+ export interface Bass {
103
+ targetbass: number;
104
+ actualbass: number;
105
+ available?: boolean;
106
+ }
107
+ export interface Tone {
108
+ targettreble: number;
109
+ actualtreble: number;
110
+ targetbass: number;
111
+ actualbass: number;
112
+ available?: boolean;
113
+ }
package/dist/types.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for Bose SoundTouch API responses
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.KeyValue = exports.PlayStatus = void 0;
7
+ var PlayStatus;
8
+ (function (PlayStatus) {
9
+ PlayStatus["PLAY_STATE"] = "PLAY_STATE";
10
+ PlayStatus["PAUSE_STATE"] = "PAUSE_STATE";
11
+ PlayStatus["STOP_STATE"] = "STOP_STATE";
12
+ PlayStatus["BUFFERING_STATE"] = "BUFFERING_STATE";
13
+ })(PlayStatus || (exports.PlayStatus = PlayStatus = {}));
14
+ var KeyValue;
15
+ (function (KeyValue) {
16
+ KeyValue["POWER"] = "POWER";
17
+ KeyValue["PLAY"] = "PLAY";
18
+ KeyValue["PAUSE"] = "PAUSE";
19
+ KeyValue["PREV_TRACK"] = "PREV_TRACK";
20
+ KeyValue["NEXT_TRACK"] = "NEXT_TRACK";
21
+ KeyValue["THUMBS_UP"] = "THUMBS_UP";
22
+ KeyValue["THUMBS_DOWN"] = "THUMBS_DOWN";
23
+ KeyValue["BOOKMARK"] = "BOOKMARK";
24
+ KeyValue["PRESET_1"] = "PRESET_1";
25
+ KeyValue["PRESET_2"] = "PRESET_2";
26
+ KeyValue["PRESET_3"] = "PRESET_3";
27
+ KeyValue["PRESET_4"] = "PRESET_4";
28
+ KeyValue["PRESET_5"] = "PRESET_5";
29
+ KeyValue["PRESET_6"] = "PRESET_6";
30
+ KeyValue["AUX_INPUT"] = "AUX_INPUT";
31
+ KeyValue["SHUFFLE_OFF"] = "SHUFFLE_OFF";
32
+ KeyValue["SHUFFLE_ON"] = "SHUFFLE_ON";
33
+ KeyValue["REPEAT_OFF"] = "REPEAT_OFF";
34
+ KeyValue["REPEAT_ONE"] = "REPEAT_ONE";
35
+ KeyValue["REPEAT_ALL"] = "REPEAT_ALL";
36
+ KeyValue["ADD_FAVORITE"] = "ADD_FAVORITE";
37
+ KeyValue["REMOVE_FAVORITE"] = "REMOVE_FAVORITE";
38
+ KeyValue["INVALID_KEY"] = "INVALID_KEY";
39
+ })(KeyValue || (exports.KeyValue = KeyValue = {}));
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "bose-soundtouch",
3
+ "version": "0.1.0",
4
+ "description": "A JavaScript/TypeScript library for controlling Bose SoundTouch speakers via the local REST API",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "demo": "ts-node demo.ts",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "keywords": [
14
+ "bose",
15
+ "soundtouch",
16
+ "speaker",
17
+ "audio",
18
+ "rest",
19
+ "api"
20
+ ],
21
+ "author": "Paul Grant",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/captivus/bose-soundtouch.git"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "axios": "^1.7.7",
32
+ "xml2js": "^0.6.2"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.14.10",
36
+ "@types/xml2js": "^0.4.14",
37
+ "ts-node": "^10.9.2",
38
+ "typescript": "^5.5.4"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ]
45
+ }