diff --git a/.gitignore b/.gitignore index 41bbaa3..ceff395 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,8 @@ build/Release # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git node_modules -.idea \ No newline at end of file +.idea + +# life configuration data +collectorSettings.json +dataStore diff --git a/README.md b/README.md index 8e69800..2e15ffa 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,26 @@ npm install soundtouch --save ``` Start the server to make use of the HTTP API ```bash -git clone https://github.com/CONNCTED/SoundTouch-NodeJS.git +git clone https://github.com/thevassiliou/SoundTouch-NodeJS.git cd SoundTOuch-NodeJS npm install node server.js ``` ## Usage -TODO \ No newline at end of file +Besides the provisioning of the Soundtouch API to control a soundtouch system ([guide] (https://github.com/Adeptive/SoundTouch-NodeJS/wiki) it offers the function to randomly select an album from a predfined set of albums and store it in a given preset. + +### Using Episode Selector +http://127.0.0.1:5006/auto/episodeSelector/:presetKey? + +http://127.0.0.1:5006/auto/getAllEpisodes + +### episodeSelector +with '''auto/episodeSelector/:presetKey?''' you can instruct the system to store a randomly selected on a given preset, or #6 if none is given. +The dataset from which the episodes are selected is stored in the '''dataStore/libraryContent.json'''. + +The content of this database can be retrieved via '''auto/getAllEpisodes'''. + +### episodeCollector +The database of episodes can be filled via the episodeCollector. +You start the episodeCollector vial '''node episodeCollector.js''' The episodeCollector listens on changes on the given device (in collectorSetting.json (example in collectorSettingsExample.json)). EpisodeCollector stores in the database every album that will be started on the device, and that serves as the dataset for '''episodeSelector''' diff --git a/api.js b/api.js index 4115f4c..eb5c7bc 100644 --- a/api.js +++ b/api.js @@ -59,12 +59,12 @@ SoundTouchAPI.prototype.getInfo = function(handler) { }; SoundTouchAPI.prototype.isAlive = function(handler) { - this.getNowPlaying(function(json){ + this.getNowPlaying(function(json) { if (json == undefined) { handler(false); return; } - var isAlive = json.nowPlaying.source != SOURCES.STANDBY; + var isAlive = json.nowPlaying.source != SOURCES.STANDBY; if (isAlive) { isAlive = json.nowPlaying.playStatus == 'PLAY_STATE'; } @@ -73,12 +73,12 @@ SoundTouchAPI.prototype.isAlive = function(handler) { }; SoundTouchAPI.prototype.isPoweredOn = function(handler) { - this.getNowPlaying(function(json){ + this.getNowPlaying(function(json) { if (json == undefined) { handler(false); return; } - var isAlive = json.nowPlaying.source != SOURCES.STANDBY; + var isAlive = json.nowPlaying.source != SOURCES.STANDBY; handler(isAlive); }); }; @@ -99,7 +99,8 @@ SoundTouchAPI.prototype.select = function(source, type, sourceAccount, location, throw new Error("Source is not optional, provide a source from the SOURCES list."); } - var data = '' + + var data = '' + '' + 'Select using API' + '' + ''; @@ -179,7 +180,19 @@ SoundTouchAPI.prototype.powerOff = function(handler) { * @param handler function (required) */ SoundTouchAPI.prototype.setPreset = function(presetNumber, handler) { - this.pressKey("PRESET_" + presetNumber, handler); + var key = "PRESET_" + presetNumber; + + var press = "" + key + ""; + var release = "" + key + ""; + + var api = this; + + api._setForDevice("key", press, function(json) { + // Because of lack of documentation from Bose using 1sec to make sure + var waitTill = new Date(new Date().getTime() + 2 * 1000); + while (waitTill > new Date()) {} + api._setForDevice("key", release, handler); + }); }; SoundTouchAPI.prototype.pressKey = function(key, handler) { @@ -211,7 +224,7 @@ SoundTouchAPI.prototype.removeZoneSlave = function(members, handler) { SoundTouchAPI.prototype._zones = function(action, members, handler) { var item = {}; - + // the below line looked like it might have been a copy/paste error from discovery.js? master is undefined here. // item.master = master; var data = ''; @@ -222,7 +235,7 @@ SoundTouchAPI.prototype._zones = function(action, members, handler) { item.slaves = []; item.slaves.push(member); data += '' + member + ''; - } else { + } else { item.slaves.push(member); data += '' + member + ''; } @@ -353,10 +366,10 @@ SoundTouchAPI.prototype.setRecentsUpdatedListener = function(handler) { }; /* -****** UTILITY METHODS *********** + ****** UTILITY METHODS *********** */ -SoundTouchAPI.prototype._getForDevice = function (action, callback) { +SoundTouchAPI.prototype._getForDevice = function(action, callback) { var device = this.getMetaData(); http.get(device.url + "/" + action, function(response) { parser.convertResponse(response, function(json) { @@ -369,10 +382,10 @@ SoundTouchAPI.prototype._getForDevice = function (action, callback) { }); }; -SoundTouchAPI.prototype._setForDevice = function (action, data, handler) { +SoundTouchAPI.prototype._setForDevice = function(action, data, handler) { var device = this.getDevice(); - var options = { + var options = { url: device.url + '/' + action, form: data }; diff --git a/collectorSettingsExample.json b/collectorSettingsExample.json new file mode 100644 index 0000000..3df1d3f --- /dev/null +++ b/collectorSettingsExample.json @@ -0,0 +1,3 @@ +{ + "deviceToListen": "YOUR DEVICE NAME" +} diff --git a/episodeCollector.js b/episodeCollector.js new file mode 100644 index 0000000..35d2dd7 --- /dev/null +++ b/episodeCollector.js @@ -0,0 +1,55 @@ +var soundTouchDiscovery = require('./discovery'); +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var store = require('data-store')('libraryContent', { + cwd: 'dataStore' +}); + +// Store your setting better in collectorSettings.json +// start from collectorSettingsExample.json by copying +var settings = { + deviceToListen: 'Office' +}; + + +// load user settings +try { + var userSettings = require(path.resolve(__dirname, 'collectorSettings.json')); +} catch (e) { + console.log('No collectorSetting.json file found, will only use default settings'); +} + +if (userSettings) { + for (var i in userSettings) { + settings[i] = userSettings[i]; + } +} +console.log("Listening to device: " + settings.deviceToListen); +console.log(store.get()); + +soundTouchDiscovery.search(function(deviceAPI) { + deviceAPI.socketStart(); + deviceAPI.setNowPlayingUpdatedListener(function(json) { + if (deviceAPI.name === settings.deviceToListen) { + if (json.nowPlaying.ContentItem != undefined) { + console.log('We received ', json.nowPlaying.ContentItem); + // we do not want to store duplicate items + if (!store.has(json.nowPlaying.ContentItem.location)) { + var contentItem = json.nowPlaying.ContentItem ; + // NOTE: Would check for isPresetable, but there are cases where even + // it is not presetable (like AUX) isPresetable is set to true + console.log('Storing location for: ', contentItem.itemName); + store.set(contentItem.location, { + source: contentItem.source, + sourceAccount: contentItem.sourceAccount, + name: contentItem.itemName + }); + } + } + } + + }); + + soundTouchDiscovery.stopSearching(); +}); diff --git a/package.json b/package.json index 22b5376..5d0766b 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "soundtouch", "version": "4.0.1", - "description": "Bose SoundTouch node js API", + "private": true, + "description": "Bose SoundTouch node js API as Theos fork", "main": "discovery.js", "scripts": { - "test": "make test" + "test": "make test", + "start": "node server.js" }, "author": "CONNCTED", "license": "ISC", @@ -13,6 +15,7 @@ "mocha": "^3.2.0" }, "dependencies": { + "data-store": "^1.0.0", "express": "^4.15.2", "mdns": "^2.3.3", "net": "^1.0.2", diff --git a/package/episodeSelector.js b/package/episodeSelector.js new file mode 100644 index 0000000..b33baea --- /dev/null +++ b/package/episodeSelector.js @@ -0,0 +1,142 @@ +var http = require('http'); +var parser = require('../utils/xmltojson'); +var SOURCE = require('../utils/types').Source; +var store = require('data-store')('libraryContent', { + cwd: 'dataStore' +}); + +const ALLIDS = store.get(); +const ALLIDS_SIZE = _getStoreLength(ALLIDS); +var PRESET_KEY_NO_DEFAULT = 6; + +var originalVolumen; + +function _wait(msec) { + var waitTill = new Date(new Date().getTime() + msec); + while (waitTill > new Date()) {} +} + +function _randomIntInc(low, high) { + return Math.floor(Math.random() * (high - low + 1) + low); +} + +function _getStoreLength(store) { + var i = 0; + for (var j in store) { + i++; + } + return i; +} + +function _getElement(n, store) { + var i = 0; + for (var j in store) { + i++; + if (i === n) { + return j + } + } +} + + +function _storeActualVolume(json) { + originalVolumen = json.volume.actualvolume; +} + +function reduceVolume(device, req, res) { + device.getVolume(_storeActualVolume); + device.setVolume(1, function(json) {}) +} + +// TODO: Document this +// localhost:5006/Kueche/auto/episodeSelector/?presetkey=5 + +function selectRandomEpisode(device, req, res, location) { + var theEpisodeNo = _randomIntInc(1, ALLIDS_SIZE); + var theEpisodeElement = _getElement(theEpisodeNo, ALLIDS) + var theEpisodeContent = store.get(theEpisodeElement); + var presetKey = req.params.presetKey; + + reduceVolume(device, req, res); + + if (presetKey) { + if (presetKey >= 1 && presetKey <= 6) { + PRESET_KEY_NO = presetKey; + } else { + // 416 Requested range not satisfiable + res.status(416).json({ + message: "presetKey should be between 1 and 6" + }); + return; + } + } else { + PRESET_KEY_NO = PRESET_KEY_NO_DEFAULT; + } + + device.select(theEpisodeContent.source, undefined, theEpisodeContent.sourceAccount, theEpisodeElement, + function(json) { + _wait(1000); // wait a second after started playing + device.stop(function() {}); + device.setPreset(PRESET_KEY_NO, + function(json) { + device.stop(function() { + device.setVolume(originalVolumen, function() {}); + }); + } + ) + } + ); + res.json({ + album: theEpisodeContent.name, + entry: theEpisodeElement, + source: theEpisodeContent.source, + sourceAccount: theEpisodeContent.sourceAccount, + presetKey: PRESET_KEY_NO + }); + +} + +function episodeMove(device, req, res, location) { + var from = parseInt(req.params.from, 10); + var to = parseInt(req.params.to, 10); + + reduceVolume(device, req, res); + + if ((from == to)) { + // 416 Requested range not satisfiable + res.status(416).json({ + message: "from and to should be different" + }); + return; + } else if (from < 1 || from > 6 || to < 1 || to > 6) { + // 416 Requested range not satisfiable + res.status(416).json({ + message: "from and/or to preset should be between 1 and 6" + }); + return; + + } + + // start playing _old + var _fromKey = "PRESET_" + from; + + device.pressKey(_fromKey, function() { + device.stop(function() {}); + device.setPreset(to, function() {}); + _wait(1000); + res.json({ + from: from, + to: to + }); + }); +} + +function getAllEpisodes(discovery, req, res) { + res.json(ALLIDS); +} + +module.exports = function(api) { + api.registerRestService('/auto/getAllEpisodes', getAllEpisodes); + api.registerDeviceRestService('/auto/episodeSelector/:presetKey?', selectRandomEpisode); + api.registerDeviceRestService('/auto/episodeMove/:from/:to', episodeMove); +}; diff --git a/package/smart_zones.js b/package/smart_zones.js index afff9f6..6686ef8 100644 --- a/package/smart_zones.js +++ b/package/smart_zones.js @@ -71,11 +71,11 @@ function _processNowPlayingList(discovery, req, res) { } function _isValidSource(source) { - return (source == SOURCE.INTERNET_RADIO - || source == SOURCE.PANDORA + return (source == SOURCE.PANDORA || source == SOURCE.DEEZER || source == SOURCE.IHEART || source == SOURCE.SPOTIFY + || source == SOURCE.TUNEIN ); } diff --git a/utils/types.js b/utils/types.js index 6053309..3e07700 100644 --- a/utils/types.js +++ b/utils/types.js @@ -1,7 +1,6 @@ module.exports = { Source: { SLAVE_SOURCE: "SLAVE_SOURCE", - INTERNET_RADIO: "INTERNET_RADIO", PANDORA: "PANDORA", AIRPLAY: "AIRPLAY", STORED_MUSIC: "STORED_MUSIC", @@ -12,7 +11,8 @@ module.exports = { UPDATE: "UPDATE", DEEZER: "DEEZER", SPOTIFY: "SPOTIFY", - IHEART: "IHEART" + IHEART: "IHEART", + TUNEIN: "TUNEIN" }, Keys: { PLAY: "PLAY",