soundtouch-api
Version:
SoundTouch API using TypeScript
283 lines (250 loc) • 9.34 kB
text/typescript
'use strict';
import {Info, infoFromElement} from './info';
import {APIErrors, errorFromElement} from './error';
import {Endpoints} from './endpoints';
import {Builder as XMLBuilder, convertableToString, OptionsV2, parseString} from 'xml2js';
import {compactMap, XMLElement} from './utils';
import {PlayInfo} from './play-info';
import {KeyState, KeyValue} from './special-types';
import {NowPlaying, nowPlayingFromElement} from './now-playing';
import {Volume, volumeFromElement} from './volume';
import {Sources, sourcesFromElement} from './source';
import {Zone, zoneFromElement, zoneToElement} from './zone';
import {BassCapabilities, bassCapabilitiesFromElement} from './bass-capabilities';
import {Bass, bassFromElement} from './bass';
import {Preset, presetFromElement} from './preset';
import {Group, groupFromElement} from './group';
import {promisify} from 'util';
import {ContentItem, contentItemToElement} from './content-item';
import axios, {AxiosInstance} from "axios";
const parseXML = promisify((xml: convertableToString, options: OptionsV2, cb: (err: Error | null, res: any) => void) => parseString(xml, options, cb));
export interface Device {
ip: string;
port: number;
name: string;
host?: string;
mac?: string;
manufacturer?: string;
model?: string;
}
export class API {
private readonly device: Device;
private readonly builder: XMLBuilder;
private readonly axiosInstance: AxiosInstance;
constructor(device: Device) {
this.device = device;
this.axiosInstance = axios.create({
headers: {
'Content-Type': 'application/xml'
}
})
this.builder = new XMLBuilder();
}
async getInfo(): Promise<Info | undefined> {
const element = await this._get(Endpoints.info);
if(element.hasChild('info')) {
const info = element.getChild('info');
if(info) {
return infoFromElement(info);
}
}
return undefined;
}
async setVolume(value: number): Promise<boolean> {
const element = await this._post(Endpoints.volume, {
volume: value
});
return element ? element.getText('status') !== undefined : false;
}
async getVolume(): Promise<Volume | undefined> {
const element = await this._get(Endpoints.volume);
if(element.hasChild('volume')) {
const volume = element.getChild('volume');
if(volume) {
return volumeFromElement(volume);
}
}
return undefined;
}
async speaker(playInfo: PlayInfo): Promise<boolean> {
const element = await this._post(Endpoints.speaker, {
play_info: playInfo
});
return element ? element.getText('status') !== undefined : false;
}
async pressKey(value: KeyValue): Promise<boolean> {
return this.holdKey(value, 0);
}
async holdKey(value: KeyValue, duration: number = 5000): Promise<boolean> {
const success = await this._key(KeyState.press, value);
if(!success) {
return false;
}
if(duration > 0) {
await new Promise((resolve) => setTimeout(resolve, duration));
}
return this._key(KeyState.release, value);
}
async getNowPlaying(): Promise<NowPlaying | undefined> {
const element = await this._get(Endpoints.nowPlaying);
if(element.hasChild('nowPlaying')) {
const nowPlaying = element.getChild('nowPlaying');
if(nowPlaying) {
return nowPlayingFromElement(nowPlaying);
}
}
return undefined;
}
async selectSource(contentItem: ContentItem): Promise<boolean> {
const element = await this._post(Endpoints.select, contentItemToElement(contentItem).data);
return element ? element.getText('status') !== undefined : false;
}
async getSources(): Promise<Sources | undefined> {
const element = await this._get(Endpoints.sources);
if(element.hasChild('sources')) {
const sources = element.getChild('sources');
if(sources) {
return sourcesFromElement(sources);
}
}
return undefined;
}
async getZone(): Promise<Zone | undefined> {
const element = await this._get(Endpoints.getZone);
if(element.hasChild('zone')) {
const zone = element.getChild('zone');
if(zone) {
return zoneFromElement(zone);
}
}
return undefined;
}
async setZone(zone: Zone): Promise<boolean> {
return this._updateZone(zone, Endpoints.setZone);
}
async addZoneSlave(zone: Zone): Promise<boolean> {
return this._updateZone(zone, Endpoints.addZoneSlave);
}
async removeZoneSlave(zone: Zone): Promise<boolean> {
return this._updateZone(zone, Endpoints.removeZoneSlave);
}
async getBassCapabilities(): Promise<BassCapabilities | undefined> {
const element = await this._get(Endpoints.bassCapabilities);
if(element.hasChild('bassCapabilities')) {
const bassCapabilities = element.getChild('bassCapabilities');
if(bassCapabilities) {
return bassCapabilitiesFromElement(bassCapabilities);
}
}
return undefined;
}
async getBass(): Promise<Bass | undefined> {
const element = await this._get(Endpoints.bass);
if(element.hasChild('bass')) {
const bass = element.getChild('bass');
if(bass) {
return bassFromElement(bass);
}
}
return undefined;
}
async setBass(value: number): Promise<boolean> {
const element = await this._post(Endpoints.bass, {
bass: value
});
return element ? element.getText('status') !== undefined : false;
}
async getPresets(): Promise<Preset[] | undefined> {
const element = await this._get(Endpoints.presets);
if(element.hasChild('presets')) {
const presets = element.getChild('presets');
if(presets) {
if(presets.hasChild('preset')) {
return compactMap(presets.getList('preset'), presetFromElement);
}
}
}
return undefined;
}
async setName(value: string): Promise<Info | undefined> {
const element = await this._post(Endpoints.name, {
name: value
});
if(element.hasChild('info')) {
const info = element.getChild('info');
if(info) {
return infoFromElement(info);
}
}
return undefined;
}
async getGroup(): Promise<Group | undefined> {
const element = await this._get(Endpoints.getGroup);
if(element.hasChild('group')) {
const group = element.getChild('group');
if(group) {
return groupFromElement(group);
}
}
return undefined;
}
// PRIVATE FUNCTIONS
private static _throwAPIErrors(root: XMLElement) {
if(root.hasChild('errors')) {
const errors = root.getChild('errors');
if(errors) {
throw APIErrors.fromElement(errors);
}
} else if(root.hasChild('Error')) {
const errElement = root.getChild('Error');
if(errElement) {
const err = errorFromElement(errElement);
if(err) {
throw new APIErrors([err]);
}
}
}
}
private async _req(method: string, endpoint: Endpoints, body?: object): Promise<XMLElement> {
const url = `http://${this.device.ip}:${this.device.port}/${endpoint}`;
let xml;
try {
if(method === 'GET') {
xml = (await this.axiosInstance.get(url)).data;
} else {
xml = (await this.axiosInstance.post(url, this.builder.buildObject(body))).data;
}
} catch(err: any) {
if(!err.response) {
throw err;
}
xml = err.response.data;
}
const data = await parseXML(xml, { trim: true });
const root = new XMLElement(data);
API._throwAPIErrors(root);
return root;
}
private _get(endpoint: Endpoints): Promise<XMLElement> {
return this._req('GET', endpoint);
}
private _post(endpoint: Endpoints, body: object): Promise<XMLElement> {
return this._req('POST', endpoint, body);
}
private async _key(state: KeyState, value: KeyValue): Promise<boolean> {
const element = await this._post(Endpoints.key, {
key: {
$: {
state: state,
sender: 'Gabbo'
},
_: value
}
});
return element ? element.getText('status') !== undefined : false;
}
private async _updateZone(zone: Zone, endpoint: Endpoints): Promise<boolean> {
const element = await this._post(endpoint, zoneToElement(zone).data);
return element ? element.getText('status') !== undefined : false;
}
}