From 5c333e1c2b96a41349d130e00065c3db2e053e62 Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Tue, 20 Feb 2018 16:49:08 +0100 Subject: [PATCH 01/14] added a 1sec delay between keyPressed and keyReleased when storing a preset. --- api.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/api.js b/api.js index 4115f4c..6ea0c62 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' + '' + ''; @@ -189,6 +190,9 @@ SoundTouchAPI.prototype.pressKey = function(key, handler) { 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() + 1 * 1000); + while (waitTill > new Date()) {} api._setForDevice("key", release, handler); }); }; @@ -211,7 +215,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 +226,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 +357,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 +373,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 }; From ff4cd7237986bd52026ca86f57b01cc00b14251d Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 10:55:12 +0100 Subject: [PATCH 02/14] ignoring config files and dataStore directory. --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 4cce59b507e3cc79a6c3c5b2e55cfbec0abea432 Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 10:55:33 +0100 Subject: [PATCH 03/14] initial checking of a configuration example file --- collectorSettingsExample.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 collectorSettingsExample.json 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" +} From 5fde12e7cdca72748f80055eb06644bd3e54d324 Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 10:56:02 +0100 Subject: [PATCH 04/14] initial checking of the random episodeSelector functionality --- episodeCollector.js | 56 ++++++++++++++++++++++ package/episodeSelector.js | 95 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 episodeCollector.js create mode 100644 package/episodeSelector.js diff --git a/episodeCollector.js b/episodeCollector.js new file mode 100644 index 0000000..0e09a29 --- /dev/null +++ b/episodeCollector.js @@ -0,0 +1,56 @@ +var soundTouchDiscovery = require('./discovery'); +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var store = require('data-store')('libraryContent', { + cwd: 'dataStore' +}); + +// Your configuration here +// Default settings +// 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) { + // 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/episodeSelector.js b/package/episodeSelector.js new file mode 100644 index 0000000..1c191a1 --- /dev/null +++ b/package/episodeSelector.js @@ -0,0 +1,95 @@ +var http = require('http'); +var parser = require('../utils/xmltojson'); +var SOURCE = require('../utils/types').Source; +var store = require('data-store')('libraryContent', { + cwd: 'dataStore' +}); + +var util = require('util'); + +const ALLIDS = store.get(); +const ALLIDS_SIZE = _getStoreLength(ALLIDS); +var PRESET_KEY_NO = 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); + + reduceVolume(device, req, res); + if (req.query.presetkey != undefined) { + PRESET_KEY_NO = req.query.presetkey; + } + 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 getAllEpisodes(discovery, req, res) { + res.json(ALLIDS); +} + +module.exports = function(api) { + api.registerRestService('/auto/getAllEpisodes', getAllEpisodes) + api.registerDeviceRestService('/auto/episodeSelector', selectRandomEpisode); +}; From a19b2555cb6ef2e35bd7d603bb89aa92a2fa0e45 Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 10:56:41 +0100 Subject: [PATCH 05/14] dependencies for episodeSelector functionality added --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 22b5376..ecf3dca 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "mocha": "^3.2.0" }, "dependencies": { + "data-store": "^1.0.0", "express": "^4.15.2", "mdns": "^2.3.3", "net": "^1.0.2", From 96619b917be429389540f84abeeab4793656690e Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 10:56:41 +0100 Subject: [PATCH 06/14] Added preset short hand notation function --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 22b5376..ecf3dca 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "mocha": "^3.2.0" }, "dependencies": { + "data-store": "^1.0.0", "express": "^4.15.2", "mdns": "^2.3.3", "net": "^1.0.2", From 8501a72416d07f50113cc6b0d81ade7ff3fda44b Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Wed, 21 Feb 2018 12:53:28 +0100 Subject: [PATCH 07/14] harmonized presetKey handling --- package/episodeSelector.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/package/episodeSelector.js b/package/episodeSelector.js index 1c191a1..22d088e 100644 --- a/package/episodeSelector.js +++ b/package/episodeSelector.js @@ -5,11 +5,9 @@ var store = require('data-store')('libraryContent', { cwd: 'dataStore' }); -var util = require('util'); - const ALLIDS = store.get(); const ALLIDS_SIZE = _getStoreLength(ALLIDS); -var PRESET_KEY_NO = 6; +var PRESET_KEY_NO_DEFAULT = 6; var originalVolumen; @@ -57,11 +55,24 @@ 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 (req.query.presetkey != undefined) { - PRESET_KEY_NO = req.query.presetkey; + + 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 @@ -69,7 +80,7 @@ function selectRandomEpisode(device, req, res, location) { device.setPreset(PRESET_KEY_NO, function(json) { device.stop(function() { - device.setVolume(originalVolumen, function() {}); + device.setVolume(originalVolumen, function() {}); }); } ) @@ -91,5 +102,5 @@ function getAllEpisodes(discovery, req, res) { module.exports = function(api) { api.registerRestService('/auto/getAllEpisodes', getAllEpisodes) - api.registerDeviceRestService('/auto/episodeSelector', selectRandomEpisode); + api.registerDeviceRestService('/auto/episodeSelector/:presetKey?', selectRandomEpisode); }; From f5d3433c813bbec1fda1cbe12ca9a512404a8ea0 Mon Sep 17 00:00:00 2001 From: Theo Vassiliou Date: Sat, 10 Nov 2018 16:25:42 +0100 Subject: [PATCH 08/14] Update README.md Added description for episodeSelector and episodeCollector --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e69800..015791e 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,25 @@ 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''' From c8b25c2d0a54a727009b090dbe62429d8b5a0b0f Mon Sep 17 00:00:00 2001 From: Theo Vassiliou Date: Sat, 10 Nov 2018 16:26:03 +0100 Subject: [PATCH 09/14] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 015791e..2e15ffa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Besides the provisioning of the Soundtouch API to control a soundtouch system ([ ### Using Episode Selector http://127.0.0.1:5006/auto/episodeSelector/:presetKey? + http://127.0.0.1:5006/auto/getAllEpisodes ### episodeSelector From 943062746b30aab632d16f930647e1b6458a0dfe Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Sat, 9 Feb 2019 14:01:34 +0100 Subject: [PATCH 10/14] added episodeMove functionality --- package/episodeSelector.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/package/episodeSelector.js b/package/episodeSelector.js index 22d088e..b33baea 100644 --- a/package/episodeSelector.js +++ b/package/episodeSelector.js @@ -96,11 +96,47 @@ function selectRandomEpisode(device, req, res, location) { } +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.registerRestService('/auto/getAllEpisodes', getAllEpisodes); api.registerDeviceRestService('/auto/episodeSelector/:presetKey?', selectRandomEpisode); + api.registerDeviceRestService('/auto/episodeMove/:from/:to', episodeMove); }; From 746e637993cb140c1fd795d231ffba9c389bf82e Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Sat, 9 Feb 2019 14:02:23 +0100 Subject: [PATCH 11/14] introduced key press holding time when setting preset --- api.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/api.js b/api.js index 6ea0c62..eb5c7bc 100644 --- a/api.js +++ b/api.js @@ -180,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) { @@ -190,9 +202,6 @@ SoundTouchAPI.prototype.pressKey = function(key, handler) { 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() + 1 * 1000); - while (waitTill > new Date()) {} api._setForDevice("key", release, handler); }); }; From bb2c12e293abe145b8eaf4078f2ac1e17023130f Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Sat, 9 Feb 2019 14:02:44 +0100 Subject: [PATCH 12/14] Added a little bit verbosity --- episodeCollector.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/episodeCollector.js b/episodeCollector.js index 0e09a29..35d2dd7 100644 --- a/episodeCollector.js +++ b/episodeCollector.js @@ -6,8 +6,6 @@ var store = require('data-store')('libraryContent', { cwd: 'dataStore' }); -// Your configuration here -// Default settings // Store your setting better in collectorSettings.json // start from collectorSettingsExample.json by copying var settings = { @@ -35,6 +33,7 @@ soundTouchDiscovery.search(function(deviceAPI) { 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 ; From 6c81755c6d86cb70a2880e8089bbb1c060ab645c Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Sat, 9 Feb 2019 14:03:09 +0100 Subject: [PATCH 13/14] added start target --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ecf3dca..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", From 9d9470eb098ca390ce397efcd3320cb792b19892 Mon Sep 17 00:00:00 2001 From: Theofanis Vassiliou-Gioles Date: Fri, 22 Feb 2019 21:26:58 +0100 Subject: [PATCH 14/14] Moved from INTERNET_RADIO to TUNEIN --- package/smart_zones.js | 4 ++-- utils/types.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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",