Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

episodeSelector functionality added #10

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ build/Release
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules

.idea
.idea

# life configuration data
collectorSettings.json
dataStore
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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'''
37 changes: 25 additions & 12 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand All @@ -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);
});
};
Expand All @@ -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 = '<ContentItem source="' + source + '" type="' + type + '" sourceAccount="' + sourceAccount + '" location="' + location + '">' +
var data = '<ContentItem source="' + source + '" type="' + type + '" sourceAccount="' + sourceAccount +
'" location="' + location + '">' +
'<itemName>' + 'Select using API' + '</itemName>' +
'</ContentItem>';

Expand Down Expand Up @@ -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 state=\"press\" sender=\"Gabbo\">" + key + "</key>";
var release = "<key state=\"release\" sender=\"Gabbo\">" + key + "</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) {
Expand Down Expand Up @@ -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 = '<zone master="' + this.getDevice().txtRecord.MAC + '" senderIPAddress="127.0.0.1">';
Expand All @@ -222,7 +235,7 @@ SoundTouchAPI.prototype._zones = function(action, members, handler) {
item.slaves = [];
item.slaves.push(member);
data += '<member>' + member + '</member>';
} else {
} else {
item.slaves.push(member);
data += '<member>' + member + '</member>';
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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
};
Expand Down
3 changes: 3 additions & 0 deletions collectorSettingsExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"deviceToListen": "YOUR DEVICE NAME"
}
55 changes: 55 additions & 0 deletions episodeCollector.js
Original file line number Diff line number Diff line change
@@ -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();
});
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
142 changes: 142 additions & 0 deletions package/episodeSelector.js
Original file line number Diff line number Diff line change
@@ -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);
};
4 changes: 2 additions & 2 deletions package/smart_zones.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Loading